Chapter 22. Integrating Code Libraries

There are many useful open-source and commercial Java libraries available today, as well as libraries developed by individuals or teams. Using these in an OSGi context can be hampered by two issues: packaging as bundles and the use of historical Java extensibility mechanisms.

The last few years have seen a dramatic increase in the number of Java libraries that include OSGi markup, but even the most OSGi-biased developer would concede that the majority of Java libraries out there are still not shipped as bundles. Even if they were bundles, many libraries use context class loaders and other techniques that clash with OSGi. This chapter discusses the integration of these libraries into the OSGi runtime environment.

Bundling, the generic term for converting a JAR to a bundle, is typically a straightforward process, but there are choices to be made and issues to be resolved. In this chapter we discuss the different bundling variants and common problems that arise when using existing code in an OSGi system. In particular, we show you how to

• Structure bundles differently

• Bundle by injection—add bundle metadata to existing JARs

• Bundle by wrapping—wrap JARs with bundle metadata

• Bundle by reference—add bundle metadata beside existing JARs without affecting the JARs, their original location, or their surrounding directory structure

• Find other bundling technology such as bnd

• Solve common class loading problems

22.1 JARs as Bundles

As we saw in Section 2.3, “The Anatomy of a Bundle,” a typical bundle is simply a JAR, as shown in Figure 22-1. Such a bundle is like any other JAR with the exception that the MANIFEST.MF contains OSGi headers to define dependencies, classpaths, and the like. In fact, if the bundle’s code does not depend on OSGi mechanisms, the JAR will work fine on normal JREs.

Figure 22-1 A traditional bundle JAR

image

Looking at it this way, it is natural for library producers to include this extra bundle metadata in their MANIFEST.MF and ship their library as both a stand-alone JAR and a bundle ready for integration into OSGi. If all libraries were shipped this way, you could skip the rest of this chapter! Library producers are increasingly including OSGi metadata, but it is still not the norm. That day may come, but in the meantime, this mind-set should help you in the process of bundling the libraries you want to use in OSGi.

Note also that there is no reason that a bundle has to be a JAR at all; that is just the norm. The Eclipse community has many examples of bundles that are delivered and run as folders. For the most part these folders are just the bundle JAR exploded on disk. Ultimately the installBundle method in OSGi takes an InputStream, so any input stream that the framework implementation understands can be used to supply bundle content. Section 23.3, “The Shape of Bundles,” details the benefits and drawbacks of different bundle shapes.

22.2 Bundling by Injection

As you saw in the previous section, JAR files can be used as bundles as long as they contain the required metadata. Here we look at how to bundle existing code library JARs by injecting this information into the manifest. This approach retains all the benefits of JAR’d bundles and increases the chances that the library authors will include the injected metadata directly in their original releases—it directly illustrates the simplicity of the required changes.

Figure 22-2 shows the process of bundling Apache Commons Logging. On the left are some original JARs from Apache and on the right is a bundle composed of the Apache JARs. Note that the original JARs had MANIFEST.MF files—all JARs do—but these did not contain OSGi markup. The operation adds the required OSGi bundle definition information in the MANIFEST.MF file of the output bundle.

Figure 22-2 Injecting metadata into a code library

image

Commons Logging comes in a number of different JARs, none of which are bundles. We could bundle these individually, but it turns out that some overlap in different ways. For example, the package org.apache.commons.logging.impl appears in multiple JARs. It would be better to combine these into one bundle.

The process for bundling individual JARs or groups is the same and is outlined here:

• Create a new bundle project using File > New > Project... > Plug-in from existing JARs.

• Ensure that the Unzip the JAR archives into the project box is checked so that the wizard unpacks all the JARs as they are imported into the new project. If more than one JAR is listed, as in Figure 22-2, they are merged as if they were on the classpath in the order specified in the wizard; that is, resources in subsequent JARs do not overwrite resources in previous JARs. The wizard then generates a manifest that exports all the packages in the new bundle.

• Click Finish.

The resultant project is just like any other bundle project. You can leave it in the workspace and code against it, you can run with it, and you can export it. A handy trick is to export it and add it to the target platform. You can then delete the project from the workspace—the library becomes just another bundle that you are using. This keeps your workspace clean and allows the new bundle to be shared between workspaces.

Where there are multiple libraries to be bundled, you can merge them as just described or convert each to a bundle individually. This is certainly feasible, but it is not always the best choice. For example, Ant comes as a set of about 28 JARs. Many of these are tiny (<10K). While the overhead of a bundle is small, this feels too fine-grained for most use cases—vast numbers of bundles are harder to manage.

Bundling closely related JARs separately can be a problem when the packages they contain overlap, as we saw previously. If code in these package fragments needs to see package-private members that are in other JARs, bundling separately does not work. Normally these JARs would all be loaded by the same class loader, so there would be only one definition of any given package. In an OSGi-based system, each bundle gets its own class loader, so the package org.apache.commons.logging.impl loaded by Bundle A is actually different from the one with the same name but loaded by Bundle B. They do not share package visibility. Whether or not this is an issue is specific to the code being bundled.

Overall the best practice is to bundle each library individually, but in certain cases that may not be possible or optimal.

22.3 Bundling by Wrapping

Since injecting metadata as described in the previous section requires modification of the original JARs, it is not always feasible. The following list outlines the most common problems with that approach:

Licensing—Licenses sometimes explicitly state that the licensed material cannot be modified or that modifications trigger further restrictions or obligations.

Signing—JARs are often signed to prevent otherwise undetected tampering with their contents. In this use of signing, it may be possible to inject the metadata and additional files—these files are either not signed or are signed by a different signer. In other situations, signing is used to imply permissions and rights. In these cases, it is less clear that metadata injection is feasible.

Multiple JARs—Some libraries come as multiple JARs. As discussed previously, the JARs can be bundled separately or they can be combined. Both approaches are feasible but may not be attractive in some cases.

If one of these situations applies to you, consider wrapping the JARs with a bundle definition; that is, create a MANIFEST.MF that describes the dependencies, and then collect the manifest and the JARs together in a single JAR, as shown in Figure 22-3.

Figure 22-3 Wrapping a code library

image

The same bundling wizard used to inject metadata can be used to wrap JARs. Simply unchecking the Unzip the JAR archives into the project box tells the wizard to copy the JARs into the project without extracting their contents. The JARs are then listed on the bundle’s classpath in the order in which you added them to the wizard. Again, the resultant project is just like any other bundle project.

Note that when you export the project, however, you have an additional choice to make. If you export the project as a JAR, you will end up with the original library JARs nested inside the new bundle JAR. Bundles in this layout are usable but are inefficient with respect to disk space, as the nested JARs must be extracted before being used—Java class loaders are not able to load classes directly from nested JARs. Furthermore, standard Java compilers cannot compile against nested JARs.

The alternative is to export the bundle as a folder. Folder bundles are supported by most frameworks, but not all provisioning systems are able to install and manage such bundles. See Section 23.3, “The Shape of Bundles,” for details on the pros and cons of various bundle shapes.

22.4 Bundling by Reference

In some situations, installed JARs cannot be moved, let alone modified. This typically happens when the libraries are delivered as part of another product and are laid down by an installer. The JARs are in a specific spot and are expected to be there to be found by other programs.

The bundling approaches outlined so far do not work because they modify either the JAR itself or its surroundings. For example, wrapping adds bundle metadata beside the JAR being wrapped. If there is only one set of JARs to wrap in a directory, the generated metadata can be directly added to the directory—essentially wrapping in place. If there are multiple libraries in the same directory, the metadata files conflict with each other. Metadata injection can be used only if the issues mentioned earlier are not applicable. For example, the JAR has to be writable.

Even if injection or wrapping is used, there is still the problem of how to get the resultant bundle installed into the framework. Many management systems expect to control bundle location. For example, the traditional Eclipse pattern is to have bundles either in the main plugins directory in the Eclipse install or in a plugins directory in an extension location. Both approaches require moving the newly bundled library. Some frameworks allow you to explicitly list bundles in configuration files such as config.ini in Equinox, but that is cumbersome and hard to manage. See Chapter 23, “Advanced Topics,” for a discussion of the osgi.bundles property and related topics.

What you really need is to have the metadata on the side and break the connection between the OSGi metadata location and the bundle content. For example, suppose you have a façade bundle JAR that contains just the metadata and indicates the location of the code JARs. The façade can be installed, updated, and run using normal OSGi API and management mechanisms without affecting, or being affected by, the referenced code libraries.

To illustrate how this works, consider a mythical Java database connectivity (JDBC) driver JAR that comes with a database product. The product installer puts jdbc.jar in c:dbdriversjdbc.jar and you cannot modify it, move it, or add files to the drivers directory.

To set this up, proceed as though you are using the wrapping approach from the previous section. Run the New Project wizard and create a JDBC bundle based on jdbc.jar. Don’t worry about the libraries being copied into the project; you can use them to do your normal development.

When you go to run your application, you need to use the original JDBC libraries. Use the following steps to set up the structure shown in Figure 22-4:

• Export the newly created JDBC bundle from your workspace to your target’s plugins directory.

• Delete the exported JARs and extraneous files (e.g., .project) from the exported target bundle.

• Edit the exported target bundle’s MANIFEST.MF and change the Bundle--Classpath header to point to the original JARs using absolute file system paths. For example, replace jdbc.jar with external:$JDBC_HOME$/drivers/jdbc.jar. You can use environment or system properties, or full file system paths, to identify the desired JAR.

• In the IDE, use Window > Preferences... > Bundle Development > Target Platform > Reload to refresh the target and add the new JDBC bundle.

• Set up an OSGi Application launch configuration to run your product. On the Bundles page, select the third option, Choose bundles and fragments to launch from the list.... In the list of bundles, uncheck the JDBC bundle in the Workspace Bundles list and check the one in the Target Bundles list.

• Run the launch configuration. It is difficult to tell which JAR is being used, but it should be the original c:dbdriversjdbc.jar. You can confirm this by renaming the original JAR and running. The application should fail when trying to load JDBC classes.

Figure 22-4 External bundle JARs

image

When it comes time to deploy your application and the JDBC bundle, you have to rely on an installer to set up the bundle’s manifest and ensure that jdbc.jar is in fact installed. The task is quite a bit easier if the database product defines environment variables or Java system properties, as shown in the example, to describe the location of its install. For example, if the product defined JDBC_HOME as an environment variable, you can set up the JDBC bundle’s manifest to include the line

Bundle-Classpath: external:$JDBC_HOME$/drivers/jdbc.jar

This mechanism has the added benefit that the JDBC bundle can be built and delivered using standard Eclipse mechanisms. Variables make this even easier.

The real danger in using this setup is the potential for mismatching the metadata and contents of the JARs. For example, you might generate the metadata based on version 3 of the JDBC drivers, but the actual installed drivers are version 2. Tracking down these kinds of bugs is challenging, to say the least. Nonetheless, the mechanism is there, and it solves some real problems. Use it with caution and care and only when absolutely necessary.

22.5 Bundling Using bnd

As an alternative to using PDE or for integration in headless builds, you can use bnd. bnd is a tool for creating bundles from Java artifacts. It is quite flexible and integrates on the command line, in Eclipse, and in Ant and Maven. bnd offers quite a number of directives for how it finds, analyzes, and collects Java artifacts. For full details see www.aqute.biz/Code/Bnd. Here we describe it in high-level detail to give a sense of what it does and how it works.

At its core, bnd is similar to the PDE function described previously. It reads the binary class files and looks for references to packages. Your role as the user is to describe the kinds of packages to be in the bundle. bnd then finds and exports all such packages. Packages that are referenced in the code, but are not in the export list, are added to the Import-Package list. The bnd technique offers some additional flexibility for collecting artifacts from a number of places.

22.6 Troubleshooting Class Loading Problems

Most code libraries are quite straightforward to bundle and then use in OSGi-based systems. You’ve seen that the wizard to create bundles from existing JARs does most of the work for you. But what happens if there are problems after bundling? At this point there are two main problems that could occur. The first happens at compile time—classes in the bundled JAR may not be visible. This is easily addressed by ensuring that the bundle exports all the necessary packages from the library and contains the correct class versions. But what if something goes wrong at runtime? The classic symptoms are ClassNotFoundExceptions and NoClassDefFoundErrors showing up in the console or the log file.

This entire section is devoted to helping you understand and troubleshoot these runtime errors. Typically, they relate to the class loading structure inherent in Equinox and OSGi. The OSGi class loading strategy and mechanism are discussed in Chapter 23, “Advanced Topics,” but here we detail some standard library coding patterns and how they are handled.

22.6.1 Issues with Class.forName()

Let’s start with the classic example of ClassNotFoundException, which occurs while using a bundled code library. Consider adding logging using log4j, a popular library for managing and logging events (http://logging.apache.org/), in Toast. Using the techniques described earlier, you can bundle log4j and add it to either your workspace or target and continue development. At runtime, however, log4j throws a number of ClassNotFoundExceptions when trying to configure its appenders.

log4j is extensible in that it allows clients to supply log appenders—effectively log event handlers. Appenders are configured by naming their implementation classes in metadata files, much like the Equinox Extension Registry and OSGi Declarative Services runtime. log4j then reads these files and loads the named classes using a code pattern similar to the following snippet:

image

Class.forName is the classic mechanism for dynamic class discovery and loading. It uses the current class loader to look for and load the requested class, in this case, an appender. The current class loader is the class loader that loaded the class containing the method executing the forName call. In the preceding snippet, the current class loader is the one that loaded AppenderHelper. The net result is the same as if a reference to the appender class were compiled into createAppender, which is exactly what using Class.forName is trying to work around.

In OSGi, this is problematic because the log4j bundle typically does not depend on the bundles providing the appenders. This is actually the point—appenders are log4j’s way of allowing its function to be extended without its prior knowledge. As a result, the log4j bundle cannot load these appenders because it does not have them on its classpath.

If log4j were written as a bundle, it could, for example, use the Equinox Extension Registry and define an appenders extension point. Bundles wanting to provide extenders would then contribute executable extensions that name their appender classes, and log4j would use createExecutableExtension as described in Chapter 16, “Extensions,” rather than the code in createAppender. log4j could also use the OSGi Whiteboard Pattern discussed in Chapter 13, “Web Portal,” to discover available appender services. Unfortunately, neither is true of log4j or libraries in general, so we need an alternative.

Equinox’s buddy class loading offers an alternative integration strategy that does not require code modification. The mechanism works as follows:

• Bundles declare that they need the help of other bundles to load classes.

• They also identify the kind of help they want by specifying a buddy policy. The policy defines what kinds of bundles are to be considered to be buddies as well as how (e.g., in what order) they are consulted.

• When a bundle fails to find a desired class through all the normal routes—Import-Package, Require-Bundle, and local classes—as outlined in Section 23.9, “Class Loading,” its buddy policy is invoked.

• The invoked policy discovers a set of buddies and consults each one in turn until either the class is found or the list is exhausted.

Let’s apply the built-in registered buddy policy to the log4j case and see how it helps. In the log4j scenario, there are a relatively large number of potential clients of the logging API and a small number of clients supplying appenders. For performance and simplicity, it makes sense to limit the buddy search scope to just those supplying appenders. The simplest approach is to make those bundles explicitly register as buddies of log4j.

To set this up, first mark the log4j bundle as needing class loading help and identify the registered policy as the policy to use. The following line added to log4j’s MANIFEST.MF makes that declaration:

Eclipse-BuddyPolicy: registered

Then in each bundle that supplies appenders, add the following line to the MANIFEST.MF to register the bundle as a buddy of log4j (i.e., org.apache.log4j):

Eclipse-RegisterBuddy: org.apache.log4j

At runtime, when log4j goes to instantiate an appender using Class.forName, it first tries all of its normal OSGi prerequisites. Then, when it fails to find the appender class, each of its registered buddy bundles is asked to load the class. If all the appender bundles are registered, the appender class is sure to be found.

22.6.1.1 Built-in Buddy Policies

Equinox supplies a number of built-in policies, as summarized in Table 22-1.

Table 22-1 Built-in Buddy Policies

image

One bundle can apply several policies simply by listing them on the Eclipse-BuddyPolicy line in the MANIFEST.MF separated by commas. Equinox invokes each policy in turn until either the class is found or all policies have been consulted.

22.6.1.2 Buddy Class Loading Considerations

As powerful and useful as buddy class loading is, it is still a mechanism of last resort. There are a number of issues that you should consider carefully before using buddies in your system:

• Buddy class loading runs counter to the notion of component that OSGi attempts to maintain and is not particularly well suited to dynamic environments—particularly ones where buddies can be uninstalled.

• Buddy class loading also incurs various performance costs. For example, buddies are consulted even where they cannot help. Typical Java resource bundle loading causes up to three class load failures and some number of resource load failures before finally getting the desired resource. Each of these failures repeats a fruitless buddy search.

• Buddy loading is relatively undirected. Normally, the OSGi class loading infrastructure knows exactly where to go to find any given package—using the information gleaned from the MANIFEST.MF files eliminates all searching. Typical buddy loading policies, however, simply search successive buddies.

• It is possible that the buddy search will find the wrong class with the right name. If two buddies contain the same class, the buddy that ultimately supplies the class depends on the policy used and may in fact be ambiguous.

22.6.1.3 DynamicImport-Package versus Buddy Class Loading

Readers familiar with OSGi may be scratching their heads and asking, “What about DynamicImport-Package?” For readers who are not familiar with OSGi, DynamicImport-Package is a mechanism that allows a bundle to state its need to use a given set of packages but not force an early binding to the exporters of those packages. Rather, the binding to package exporters is done at runtime when the bundle tries to load from a dynamically imported package.

So, some Class.forName problems can be alleviated simply by adding

DynamicImport-Package: <list of packages or *>

to the MANIFEST.MF for the bundle using Class.forName. This has the following drawbacks compared to the buddy loading described previously:

• Dynamic importing is unscoped; that is, all bundles exporting packages are considered. As such, the search may include many irrelevant and unrelated bundles. By contrast, the buddy loading mechanism allows for policies that use dynamic information such as the bundle dependency graph to drive the search for classes.

• Dynamic importing implies inter-bundle constraints. When a Bundle A loads a class from a Bundle B using dynamic importing, A is then considered to be dependent on B. If B is refreshed or uninstalled, A is refreshed. This behavior is valuable for maintaining consistency when A actually uses and retains references to B’s classes. However, several serialization scenarios have A simply using B’s classes temporarily (e.g., to load some object stream)—there is no lasting dependency.

• Dynamic import considers only packages explicitly exported by other bundles. Again this can be a desirable characteristic, but in various use cases such as serialization, the importing bundle potentially needs access to classes that would normally not be exported, for example, to load instances from an object stream.

This is not to say that DynamicImport-Package should never be used, just that it should be used appropriately. For example, use it when the set of packages needed is well known and the importing bundle has a lasting dependency on the imported packages.

22.6.2 Issues with Context Class Loaders

Since Java 1.2, the Class.forName mechanism has been largely superseded by context class loading. As a result, most modern class libraries use a context class loader. In the following discussion, we show how Equinox transparently converts the use of context class loaders into something equivalent to Class.forName. Doing this allows the buddy loading and DynamicImportPackage mechanisms described previously to be used to eliminate ClassNotFoundExceptions and NoClassDefFoundErrors.

Each Thread in Java 1.2 and above has an associated context class loader field that contains a class loader. The class loader in this field is set, typically by an application container, to match the context of this current execution; that is, the field contains a class loader that has access to the classes related to the current execution (e.g., web request being processed). Libraries such as log4j access and use the context class loader with the updated AppenderHelper code pattern:

image

By default, the context class loader is set to be the normal Java application class loader—the one you specify on the Java command line. Given that, the use of the context class loader in normal Java application scenarios is equivalent to using Class.forName, and there is only one class loader involved—the normal application class loader. When running inside OSGi, however, the code pattern outlined previously fails, because

• By default, OSGi frameworks do not consult the application class loader. OSGi-based applications put their code on dynamic bundle classpaths rather than on the normal Java application classpath.

• OSGi cannot detect bundle context switches and set the context class loader as required—there is no way to tell when execution context shifts from one bundle to the next as is done in web application servers.

These characteristics, combined with the compositional nature of OSGi, mean that the value of the context class loader field is seldom useful in OSGi contexts.

Clients can, however, explicitly set the context class loader before calling libraries that use the context class loader. The following snippet shows an example of calling log4j using this approach:

image

First the current context class loader is saved. The context class loader field on the current thread is then set to an appropriate value for the current execution, and log4j is called. log4j’s AppenderHelper uses the context class loader, so in this case it uses the client’s class loader (e.g., this.getClass().getClassLoader()). When the operation is finished, the original context class loader is restored.

The assumption here is that the client’s class loader is able to load all required classes. This may or may not be true. Even if it can, the coding pattern is cumbersome to use and hard to maintain for any significant number of library calls. Ideally, log4j would be able to dynamically discover the context relevant to a particular class loading operation. Equinox enables this using the context finder.

The context finder is a type of ClassLoader that is installed by Equinox as the default context class loader when the framework is started. When invoked, the context finder searches down the Java execution stack for a class loader other than the system class loader. In the previous AppenderHelper example, it finds the log4j bundle’s class loader—the one that loaded AppenderHelper. The context finder then delegates the load request to the discovered class loader.

This mechanism effectively transforms log4j’s call to getContextClassLoader().loadClass(String) to the equivalent Class.forName call using log4j’s class loader to load the given class. Now the buddy class loading techniques discussed in Section 22.6.1 can be applied to help log4j load the needed appender classes.

The net effect is that clients of log4j do not have to use the cumbersome coding pattern outlined earlier, even though the libraries they call use the context class loader. This approach generalizes to other context class loading situations.

22.6.3 Managing JRE Classes

For various reasons, some libraries include packages that are normally found in the JRE. For example, version 2.6 of Xalan, the XML transformation engine, comes with types from the org.w3c.dom.xpath package in xalan.jar. These types are also included as part of typical JRE distributions. When xalan.jar is used as part of a normal Java application, it is added to the classpath, but its xpath classes are obscured by those in the JRE. Everything is fine.

When you bundle Xalan, you have to be careful to ensure that you produce Export-Package entries for all packages in xalan.jar. You should also be sure to add imports for the org.w3c.dom.xpath package found in the JRE. Without those imports, Xalan uses its own copies of the xpath types and may conflict with those supplied by the JRE.

This happens because OSGi class loading is highly optimized. These optimizations depend on the bundle manifest information to know which packages come from which bundles. Except for the use of certain buddy policies, the class loaders never search for classes; they always know exactly where to find them.

For the JRE packages, only java.* packages are assumed to come from the boot class loader. All others must be imported in the consuming bundle’s MANIFEST.MF. The API packages included in the JRE are typically exported by the OSGi System Bundle. In Equinox, this list is captured in a JRE profile. The org.eclipse.osgi bundle includes a number of profiles for common JREs and automatically detects the appropriate one to use. See Section 23.9, “Class Loading,” for more details.

So, if the Xalan bundle fails to import the xpath packages, its local copies are used. This may result in ClassCastExceptions because the bundle’s copy of the type is not interchangeable with the copy supplied by the JRE. Changing the bundle to import the packages tells OSGi to use the external copy. Alternatively, the offending packages can be removed from Xalan.

22.6.4 Serialization

Serialization of objects occurs in many different situations. Some libraries use the built-in java.io.Serializable mechanism directly. Some use it indirectly as a consequence of using Remote Method Invocation (RMI). Others serialize objects using their own marshaling strategies (e.g., Hibernate stores/loads objects to/from relational databases). Regardless of the technique used, these bundles have the following characteristics:

• They are typically generic utilities and do not have access to, or knowledge of, your domain classes.

• They do not hold on to the classes they request, but rather use them to load objects and then discard their references.

• They need access to internal classes if instances of internal classes have been serialized.

Dynamically loading classes using DynamicImport-Package or buddy class loading and context class loading address these problems. In effect, loading a serialized object is equivalent to the log4j appender problem. Appender classes are identified by name to log4j. Classes to load are identified to the serialization bundle by name in the object stream. In both cases, the loading bundle needs to search beyond its prerequisite bundles to find the desired classes.

As with the logging case, the ideal solution is for the library to be OSGi-aware and use services or extensions to allow a bundle with serializable objects to make itself available. This is rarely a pragmatic solution, however.

22.7 Summary

The Java world includes a wealth of useful code libraries. OSGi, Equinox, and PDE provide a number of techniques for integrating these code libraries into the runtime environment. This can be as simple as running a wizard—most of the time it is.

In some cases, however, the code in the library uses certain patterns that are at odds with OSGi’s modularity support. Class loading is the most common bone of contention. Equinox includes several mechanisms and strategies for dealing with these cases. In particular, you can use buddy loading policies or DynamicImport-Package to provide visibility to classes that would normally not be visible, and the context finder to discover possible sources of classes. In this chapter we described the most significant of these and illustrated their use. The mechanisms outlined here enable you to resolve most remaining class loading issues encountered when integrating code libraries into an OSGi-based environment.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
52.15.65.65