Largely because it allows multiple components to interact efficiently and securely in a Java Virtual Machine (JVM), the Open Service Gateway Initiative (OSGi) specification claims the title: “The Dynamic Module System for Java.” With its rich security model, OSGi allows each component to run in its own secured environment. However, by setting the proper permissions, you can enable OSGi components to reuse other components, a capability that differentiates OSGi from other containers. This is what makes the OSGi framework lightweight and reusable.
The core of OSGi is a framework that defines:
- An application lifecycle management model
- A service registry
- An execution environment
- An atomic part of the application that is composed of modules or components
These definitions standardize how you define modules or components and how you manage the lifecycle of these components. The beauty of the OSGi runtime is that you can install, update, or remove components on the fly—without interrupting existing operations. This feature makes OSGi ideal for domains where component/module provisioning must occur without interruptions to the existing operations (e.g., grid and cloud computing). OSGi also can expose Plain Old Java Object (POJO) services at each component level, a service-oriented way for components to communicate with each other.
In this article, we walk through the development of a thick client: a user login component that authenticates users before granting access to back-end resources. Along the way, we explore reusability and size reduction of individual bundles, as well as features such as OSGi services and Eclipse plug-in extension points.
Developing a Reusable User Login Solution for OSGi
The architecture for the user login solution is composed of two layers, each containing a bundle/component. One bundle comprises the UI part (UserLoginUI) and the other provides the required services (UserLoginServices) for the solution. The UserLoginUI bundle helps to collect the credentials from the user, and the UserLoginServices bundle validates and saves the credentials for further use.
The rest of the article provides instructions for developing this solution in Eclipse.
Create a New Service
First, you need to create a new service:
- Click on the File tab, select the New option, and then select the Other… option (see Figure 1).
- Select the Plug-in Development wizard and then Plug-in Project (see Figure 2).
- To create a service, you need to change the target platform. The default selection is an Eclipse version. You need to change that to an OSGI framework, select Equinox from the dropdown list, and then click Next (see Figure 3).
- Click the Finish button, and you have created a bundle having a blank service (see Figure 4).
Register Services
The User Login solution has two services:
- IValidateService: This service validates user credentials against a credential store. It can have multiple implementations, depending on the type of credential storage.
- IContextDataService: This service hides the implementation classes from the consumer of the services, leaving it to the bundle’s discretion which implementation is made available to the consumer.
Because OSGi allows updates to the bundles without interrupting dependent bundles, you can update these services in a highly dynamic manner without effecting the service consumer.
Before using a service, you first need to register it. Because your solution has two services, you need to register both. Here is a code snippet written in the Activator class to register them:
IContextDataService dataService = new UserSessionService();
IValidateService validateService = new ValidateService();
// register the service
context.registerService(IContextDataService.class.getName(), dataService, new Hashtable());
context.registerService(IValidateService.class.getName(), validateService, new Hashtable());
The context object is the shared object across all the bundles within a single Equinox runtime. The context object here refers to the UserLoginServices bundle, which registers the two services with the Eclipse Equinox runtime. Because the OSGi framework allows bundles to expose services for other bundles to use, you expose the packages that implement the functionality of the above-mentioned services.
Here is a snapshot of the MANIFEST.MF
of UserLoginServices bundle:
Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: CommonServices Plug-in
Bundle-SymbolicName: CommonServices
Bundle-Version: 1.0.0
Bundle-Activator: com.infy.setl.common.services.activator.Activator
Export-Package: com.infy.setl.common.services.genericserviceint,
com.infy.setl.common.services.usermode;uses:=”com.infy.setl.common.services.usermodelint”,
com.infy.setl.common.services.usermodelint,
com.infy.setl.common.services.userserviceint;uses:=”com.infy.setl.common.services.genericserviceint”,
com.infy.setl.common.services.validateservice,
com.infy.setl.common.services.validateserviceint
Import-Package: org.osgi.framework;version=”1.3.0″,
org.osgi.util.tracker;version=”1.3.3″
Created-By: 1.5.0_06-b05 (Sun Microsystems Inc.)
Ant-Version: Apache Ant 1.7.1
Eclipse-LazyStart: true
Require-Bundle: org.eclipse.core.runtime
The packages exposed are the ones that contain POJO services. Another developer can import these packages individually or import the whole bundle in his or her application to make use of the services. Your solution itself uses the services of this bundle through UserLoginUI to validate and cache the credentials.
UserLoginUI
Here are the steps for creating a new UI plug-in.
- Click on File tab, select the New option, and then select the Other… option (see Figure 5).
- Select the Plug-in Development wizard and then Plug-in Project (see Figure 6).
- Use the default selection and click Next (see Figure 7).
- Click the Finish button and you have created a blank new plug-in (see Figure 8).
Manifest and Activation of UI Bundle
To import service bundle classes for use in the UserLoginUI bundle, specify the following in Manifest.MF:
Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: CommonLoginAuth Plug-in
Bundle-SymbolicName: CommonLoginAuth;singleton:=true
Bundle-Version: 1.0.0
Bundle-Activator: com.infy.setl.common.login.activator.Activator
Require-Bundle: org.eclipse.ui,
org.eclipse.core.runtime,
org.eclipse.swt,
org.eclipse.ui.workbench,
org.eclipse.jface,
CommonServices;bundle-version=”1.0.0″
Eclipse-LazyStart: true
Export-Package: com.infy.setl.common.login.ui
Bundle-ClassPath: .,
org.eclipse.jface_3.3.2.M20080207-0800.jar
Import-Package: com.infy.setl.common.services.usermode,
com.infy.setl.common.services.usermodelint,
com.infy.setl.common.services.userserviceint,
com.infy.setl.common.services.validateservice,
com.infy.setl.common.services.validateserviceintYou export the IValidateService and IContextDataService classes to access the services.
Next, you install and use the service imported above if the OSGI container has not done it for you (i.e., you start the services programmatically through code). You can do this by adding the code below in the Activator class or by designing a separate class. You need to have the service bundle started because the current bundle depends on the service bundle for validation and caching services.
if(bundle!=null){
if(bundle.getState()==Bundle.ACTIVE){
context = bundle.getBundleContext();
if(context!=null){
userServiceTracker = new ServiceTracker(context,IContextDataService.class.getName(), null);
if(userServiceTracker!=null){
userServiceTracker.open();
// grab the service
userSessionService = (IUserSessionService)userServiceTracker.getService();
if(userSessionService!=null)
return userSessionService;
}
}}
else {
try {
bundle.start();
context = bundle.getBundleContext();
if(context!=null){
//context.getBundle().start();
userServiceTracker = new ServiceTracker(context,IContextDataService.class.getName(), null);
if(userServiceTracker!=null){
userServiceTracker.open();
userSessionService = (IUserSessionService)userServiceTracker.getService();
if(userSessionService!=null)
return userSessionService;
}
}
}
catch (BundleException e) {
// TODO Auto-generated catch block
//e.printStackTrace();
javaLogger.log(Level.INFO, “Bundle Not Started”,e);
}
}
} }
Plug-in Extensions
Because this bundle uses two extension points (namely, Splash Handler and Preference Pages), you need to specify the same in plug-in.xml
. The class node specifies the class name that extends the specific extension point. Below is a snapshot of plug-in.xml
:
<plugin>
<extension
point=”org.eclipse.ui.splashHandlers”>
<splashHandler
class=”com.infy.setl.common.login.ui.LoginHandler”
id=”CollaborationCommonServices”>
</splashHandler>
<splashHandlerProductBinding
splashId=”CollaborationCommonServices”
productId=”org.eclipse.platform.ide”>
</splashHandlerProductBinding>
</extension>
<extension
point=”org.eclipse.ui.preferencePages”>
<page
name=”UserConfiguration”
class=”com.infy.setl.common.login.preferences.DeWConfigurationPreferencePage”
id=”DeW.User.Preferences”>
</page>
</extension>
</plugin>
Splash Handler is an Eclipse extension point that produces a splash screen, a UI screen that appears at boot time. It is primarily provided to allow you to customize the look of the Eclipse boot time screen. As such, you can configure the screen further to have different animations, images, etc. The Splash Handler extension point definition in plug-in.xml
contains a couple of extra nodes, namely splashId and productId. All the IDs defined in the plug-in.xml
should always be unique because Equinox uses these IDs to uniquely identify a particular plug-in. During the bootstrapping process, you can customize the image location so that Eclipse picks up your defined location image for display rather than the default one.
<extension
point=”org.eclipse.ui.splashHandlers”>
<splashHandler
class=”com.infy.setl.common.login.ui.LoginHandler”
id=”CollaborationCommonServices”>
</splashHandler>
<splashHandlerProductBinding
splashId=”CollaborationCommonServices”
productId=”org.eclipse.platform.ide”>
</splashHandlerProductBinding>
</extension>
Next, you create a class extending AbstractSplashHandler. You override the init
method to customize the way Eclipse loads.
public void init(final Shell splash) {
// Store the shell
//javaLogger.entering(“In Login Handler”, “In init”);
javaLogger.log(Level.INFO, “In Init”);
super.init(splash);
// Configure the shell layout
configureUISplash();
// Create UI
initShell(splash);
createUI();
// Create UI listeners
addListeners();
// Force the splash screen to layout
splash.layout(true);
// Keep the splash screen visible and prevent the RCP application from
// loading until the close button is clicked.
closeWindow();
}
You then add a few lines of code to specify the image location for Eclipse to load.
private void configureUISplash() {
// Configure layout
javaLogger.log(Level.INFO, “In UI Splash”);
FillLayout layout = new FillLayout();
getSplash().setLayout(layout);
// Force shell to inherit the splash background
getSplash().setBackgroundMode(SWT.INHERIT_DEFAULT);
try {
getSplash().setBackgroundImage(getImage());
}
catch (IOException e) {
javaLogger.log(Level.WARNING,”Image not found”,e);
}
}
private Image getImage() throws IOException {
Bundle bundle = Platform.getBundle(ICommonConstants.MY_PLUGIN_ID);
Path path = new Path(ICommonConstants.RELATIVE_PATH_FOR_COLLABORATION_ECLIPSE_IMAGE);
URL fileURL = FileLocator.find(bundle, path, null);
InputStream in = fileURL.openStream();
return new Image(null,in);
}
Now you can add your desired number of widgets to get an output like Figure 9. In this example, you have added two textboxes and two buttons for the login screen.
Now you’re ready to move on to preference pages. The Eclipse Preference Page is a common dialog box provided where the user can configure the way Eclipse behaves. The user can set or edit his/her credentials from this page. Below is the way to configure Preference Pages in the plug-in.xml
.
<extension
point=”org.eclipse.ui.preferencePages”>
<page
name=”UserConfiguration”
class=”com.infy.setl.common.login.preferences.DeWConfigurationPreferencePage”
id=”DeW.User.Preferences”>
</page>
</extension>
You extend the PreferencePage class to add a new block to the Preference Page, and then you override the createContents method.
protected Control createContents(final Composite parent)
{
javaLogger.log(Level.INFO,”In PreferencePage”);
super.noDefaultAndApplyButton();
setParent(parent);
Composite composite = new Composite(parent, SWT.NULL);
composite.setSize(300,120);
Group groupMainShell = new Group(composite, SWT.NULL);
groupMainShell.setLayout(new GridLayout(2, true));
groupMainShell.setText(“Login Credentials”);
groupMainShell.setBounds(10, 10, 220,95);
GridData data;
labelUserName = new Label(groupMainShell, SWT.NONE);
labelUserName.setText(“UserName”);
data = new GridData(GridData.FILL_HORIZONTAL);
data.grabExcessHorizontalSpace = true;
labelUserName.setLayoutData(data);
labelUserName.setLocation(15, 30);
labelUserName.pack();
textBoxUserLogin = new Text(groupMainShell, SWT.BORDER);
data = new GridData(GridData.FILL_HORIZONTAL);
data.grabExcessHorizontalSpace = true;
textBoxUserLogin.setLayoutData(data);
textBoxUserLogin.setLocation(80, 28);
textBoxUserLogin.setSize(120, 20);
labelPassword = new Label(groupMainShell, SWT.NONE);
labelPassword.setText(“Password”);
data = new GridData(GridData.FILL_HORIZONTAL);
data.grabExcessHorizontalSpace = true;
labelPassword.setLayoutData(data);
labelPassword.setLocation(15, 60);
labelPassword.pack();
textBoxPassword = new Text(groupMainShell, SWT.BORDER|SWT.PASSWORD);
data = new GridData(GridData.FILL_HORIZONTAL);
data.grabExcessHorizontalSpace = true;
textBoxPassword.setLayoutData(data);
textBoxPassword.setLocation(80, 58);
textBoxPassword.setSize(120, 20);
// System.out.println(“In Preference Page: “+ System.currentTimeMillis());
userCreds = new UserCredentials();
userSessionService = DeWCommonAuthorisationUtil.getUserSessionObject();
if(userSessionService!=null){
if (userSessionService.get(System.getenv(“USERNAME”)) == null )
{
userCreds.setUserId(System.getenv(“USERNAME”));
textBoxUserLogin.setText(userCreds.getUserId());
}
else {
textBoxUserLogin.setText(((IUserCredentials)(userSessionService.get(System.getenv(“USERNAME”)))).getUserId());
textBoxPassword.setText(((IUserCredentials)(userSessionService.get(System.getenv(“USERNAME”)))).getPassword());
setPassword(((IUserCredentials)(userSessionService.get(System.getenv(“USERNAME”)))).getPassword());
userCreds.setUserId(((IUserCredentials)(userSessionService.get(System.getenv(“USERNAME”)))).getUserId());
}
}
textBoxPassword.addModifyListener(new ModifyListener(){
public void modifyText(ModifyEvent arg0) {
// TODO Auto-generated method stub
setPassword(textBoxPassword.getText());
}} );
return null;
}
The output of the above code would look like similar to Figure 10.
The final piece is a login pop-up window, which is a simple SWT window with user name and password text fields. It is an event-based pop-up window that collects the credentials from the user (see Figure 11).
Using the Login Solution Components
The two bundles in the Login Solution (UserLoginUI and UserLoginServices) are modular components. In this section, you’ll learn how to can plug these bundles into an application. Your solution provides services for validating and storing user credentials. As mentioned previously, to make use of the services, a developer can import packages individually or import the whole bundle in their applications. A snapshot of the UserLoginUI bundle’s MANIFEST.MF
is shown below in for the scenarios.
Here is the MANIFEST.MF
when whole bundle is imported:
Require-Bundle: org.eclipse.ui,
org.eclipse.core.runtime,
org.eclipse.swt,
org.eclipse.ui.workbench,
org.eclipse.jface,
CommonServices;bundle-version=”1.0.0″
Here it is for cases when specific packages are imported:
Import-Package: com.infy.setl.common.services.activator,
com.infy.setl.common.services.genericserviceint,
com.infy.setl.common.services.usermode,
com.infy.setl.common.services.usermodelint,
com.infy.setl.common.services.userserviceint,
com.infy.setl.common.services.validateservice,
com.infy.setl.common.services.validateserviceint
After this, you just add a few lines of code in your application and start using the the Common Services. Before using any service, you need an object of that particular service. The following snippet gets the service object of the cache services provided in the UserLoginServices bundle.
if(bundle!=null){
if(bundle.getState()==Bundle.ACTIVE){
context = bundle.getBundleContext();
if(context!=null){
userServiceTracker = new ServiceTracker(context,IContextDataService.class.getName(), null);
if(userServiceTracker!=null){
userServiceTracker.open();
// grab the service
userSessionService = (IUserSessionService)userServiceTracker.getService();
if(userSessionService!=null)
return userSessionService;
}
}
}
When you have the service object, you can invoke the services whenever you need them. Here is the invocation of the cache service.
If( userSessionService != null) {
userSessionService.save( creds.getUserId(), creds);
}
To use the UserLoginUI bundle, a user application needs to import the bundle or import the package exported by the UserLoginUI bundle. Click here to download the sample code.
Further Possibilities with OSGi and Eclipse
The Login Solution has a few other use cases to consider. For example, you could extend it for Eclipse RCP applications for which a developer needs to maintain multiple user credentials for multiple applications. In that use case, many different apps use this service bundle as middleware for login and session management. Wouldn’t it be interesting if the whole solution was multi-tenant? Then what modifications would you need to make to meet the requirements? These are open questions for you to ponder as you explore the possibilities of OSGi.
Code Download
About the Authors
Prasad Siva Sai Choutupalli is a System Engineer for Infosys Technologies Ltd., India in SETLabs (Software Engineering and Technology Labs), the R&D division of the company. His experience is in building middleware and Eclipse-based applications.
Manish Malhotra is a Technology Lead with SETLabs (Software Engineering and Technology Labs), the R&D division of Infosys Technologies Ltd. He works in the JEECoe group. Apart from designing and implementing Java- and Java EE-based applications, he has also worked on software engineering and semantic analysis research.