Creating a new Client Module

Last modified by colinw on 2021/09/10 23:34

There are typically two steps taken when developing a new Client Module: defining an appropriate interface for the module, and then providing a default implementation. As an example, let's walk through the user menu client module. See the com.xmlnamespace.panel.client.user.menu package, along with the appropriate OSGi component definition, to follow along.

Step 1: Define an appropriate interface

The interface should include all methods that the client module provides. As an example, here's the interface defined for the user menu module:

com.xmlnamespace.panel.client.user.menu.UserMenuClientModule
public interface UserMenuClientModule extends ClientModule {

public final String FACTORY_NAME = "com.xmlnamespace.panel.client.factory.client_module.user_menu";

public void addItem(final UserMenuItem item);
public void updateItem(final UserMenuItem item);
public void removeItem(final UserMenuItem item);
}

Let's step through this line by line:

  • Line 5: Notice that the interface extends the ClientModule interface. Since this will be a login client module, this is the appropriate interface to extend.
  • Line 7: The factory name defined here will be used by all menu item providers in their OSGi factory name component parameter. Although it can't be directly referenced in the component definition files, it is good practice to have this specified here so that other developers can easily find it. When developing a new module, ensure that this factory name is appropriate the first time, since it's difficult to change it later on without affecting other components.
  • Lines 9-11: These are the methods defined for the interface. In this case, the service is pretty simple, providing methods to add, update, and remove menu items from the user menu. Note that the UserMenuItem class is defined in another file. 

The interface should be in a publicly available and exported package, so that other OSGi bundles have access to it if necessary. Now, other iSymphony bundles will be able to query the ClientRootUI to load this module out. However, since we haven't defined an implementation yet, they'll get a NullPointerException when they try to use it.

Step 2: Define an implementation

The implementation class for the user menu client module is fairly complex, so we'll just look at a few key sections.

com.xmlnamespace.panel.client.user.menu.impl.UserMenuClientModuleImpl
public class UserMenuClientModuleImpl extends BaseClientModule implements UserMenuClientModule, ... {

Most client modules you develop will extend the BaseClientModule class, since it provides easy access to the ClientRootUI object (under the rootUI class member), and default startup/shutdown methods. 

@Override
public void startup(final ComponentContext context) {
super.startup(context);

We override the startup method, since we need to perform initialization logic. However, we make sure to call super.startup first, since that gives us access to the rootUI object. 

final MainWorkspaceClientModule mainWorkspace = (MainWorkspaceClientModule) rootUI.getClientModule(MainWorkspaceClientModule.class);
if (mainWorkspace != null) {
mainWorkspace.setUserMenuComponent(rootLayout);
} else {
logger.error("Cannot load main workspace.");
}

Since we need to add the user menu to the main workspace, we load the main workspace client module from the ClientRootUI. The main workspace client module provides a method to set the UI component that provides the user menu, so we use that to add our root component. And although here we're checking for the presence of the main workspace module (via the null check), later on we'll define the main workspace module as our parent in the dependency tree. Therefore, this check wasn't strictly necessary (and was probably left over from a previous version when it was).

@Override
public String getFactoryName() {
return UserMenuClientModule.FACTORY_NAME;
}
@Override
public Class<?> getClassReference() {
return UserMenuClientModule.class;
}

Here we override the two other requireds of the ClientModule interface. The factory name that's returned should be the factory name of the interface for the client module, as should the class reference that's returned. That's because other modules are expecting to get a UserMenuClientModule instance, not the full UserMenuClientModuleImpl implementation instance. 

Step 2.5: Define the OSGi component definition

Finally, we need to define the OSGi component definition, to allow OSGi to load this when necessary. See the example:

<?xml version="1.0" encoding="UTF-8"?>
<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0"
activate="startup" deactivate="shutdown"
factory="com.xmlnamespace.panel.client.factory.client_module.main_workspace" immediate="false"
name="com.xmlnamespace.panel.client.user.menu.impl.UserMenuClientModuleImpl">
   <implementation class="com.xmlnamespace.panel.client.user.menu.impl.UserMenuClientModuleImpl"/>
</scr:component>

Here there are a few things to note:

  • Line 3: Be sure to specify the startup and shutdown methods as the activate and deactivate methods, to ensure they get called correctly. 
  • Line 4: The factory name specified here should be the publicly accessible factory name for the ClientModule that you want as the parent to this module. The parent is guaranteed to be loaded before this module is loaded, due to the way the system is set up. Therefore, it is essentially a dependency (although you can only specify one). In this case, we need the main workspace module to be loaded before we are (since we need to add ourselves to it), so we specify the factory name for that module as our factory. 
  • Lines 5-6: Generally it's good practice to set the name of the implementation class as the name of the component, and of course you need to specify the implementation as well. 

If necessary, you can specify other OSGi components to inject here, although generally the client module system will provide everything. As an example, you may need the communication service injected. 

 

   
iSymphony