Friday, November 30, 2007

OSGi Services for Dynamic Applications (II)

Or: use OSGi to write a dueling card game, part 2: the Game Controller OSGi Services client
In this posting I'm going to write the Game Controller which is a client to the User Interface OSGi Services and also a client to the Card OSGi services.

The following diagram from the first posting outlines the game architecture:

The yellow triangles represent the OSGi services, the red rectangles are bundles that provide service implementations. The Game Controller is a client to the services and lives in an OSGi bundle too.

I am using OSGi Declarative Services to handle the client side of the OSGi services. I'm doing this because DS is easier than using the OSGi ServiceTracker API, and it's part of the OSGi spec.

I always find the easiest way to develop an OSGi bundle by using the Eclipse tooling that you get with the Eclipse Plugin Development Environment (download from here). With that you simply go: File -> New Project... -> Plug-in project. In the following window you simply select 'OSGi Platform' as the target platform. If you specify 'standard' it means that you won't be using any Equinox specific bits.

On the next page you can specify some details around the plugin ID, name and version. The defaults are generally fine but for this posting you won't need an activator as Declarative Services will take care of the stuff that is typically done in an activator for us, so you can deselect that option. Finally, you don't need to use any of the templates that come with Eclipse for this posting.
Hitting 'Finish' will get you an Eclipse project that is marked as a PDE project (which is an OSGi project) with a META-INF/MANIFEST.MF file which defines the attributes of the OSGi bundle.

My game bundle has two dependencies:
  • It depends on the DuelInterfaces bundle from the previous posting. All of its classes are in the game.model package, so I will be importing that package, which gives me the freedom to rename or reversion the DuelInterfaces bundle if needed.
  • Since I'm using Declarative Services it depends on a DS implementation. I'm using the one from Equinox / Eclipse, which doesn't come with Eclipse by default, you have to manually download and install it. To get it, go to the Equinox download page, select your Eclipse version (I took 3.3.1.1) and then look for the org.eclipse.equinox.ds_xxx.jar download. I downloaded this one. Then simply drop the downloaded jar file in the plugins directory of your Eclipse installation.
    Because I'm not using any specific classes from the DS implementation, I cannot use the 'Import Package' mechanism, so I've specified this bundle as a direct dependency in 'Required Plugins'.
I've entered the dependencies in my manifest, using the Eclipse manifest editor, which now looks like this.


What I want DS to do for me is tell the Game Controller when someone has registered any new Card Services in the game or when a new User Interface is registered. When a UI is added, the controller will look for another UI that is not playing a game, if there is one it will hook them up and start the game for both players. If there is no free other UI, the newly added UI will be held until another UI presents itself. This is implemented in the Controller class which contains an addUI() method like this:

public synchronized void addUI(UserInterface ui) {
  System.out.println("Adding UI for: " +
      ui.getPlayerName());
  
  if (freeUIs.size() > 0) {
    UserInterface otherUI = freeUIs.remove(0);
    
    Player p1 = new Player(otherUI, 250);
    Player p2 = new Player(ui, 250);

    // Init both UI's with players and static commands
    ui.initializeGame(p2, p1, new DrawCommand(),
        new NextPhaseCommand(), new SelfFighter(p2));
    otherUI.initializeGame(p1, p2, new DrawCommand(),
        new NextPhaseCommand(), new SelfFighter(p1));
    
    // Create a new game with all the cards available
    final Game g =
        new GameImpl(new ArrayList(cards), p1, p2);
    
    new Thread(new Runnable() {
      public void run() {
        g.play();
      }
    }).start(); // play, don't block the OSGi thread
    games.add(g);
  } else {
    freeUIs.add(ui);
  }
}


A similar, but even simpler method is called when new cards are registered, my addCard() looks like this:
public void addCard(Card card) {
  synchronized (cards) {
    cards.add(card);
  }
}


But what do I do to get my methods called when these services get registered. This is where DS comes in. I have added a DS configuration file that instantiates my Controller component and gets DS to call me back when this happens. This pattern is called Inversion of Control (IOC). I have added a DS configuration file in my Duel bundle project in OSGI-INF/controlcomponent.xml. I could really place this file anywhere in the bundle, but putting it in OSGI-INF seems to be the convention. It contains the following XML:
<component name="controller">
  <implementation class="game.Controller"/>

  <reference name="UI"
    interface="game.model.UserInterface"
    bind="addUI"
    unbind="removeUI"
    cardinality="0..n"
    policy="dynamic"/>
  <reference name="CARD"
    interface="game.model.Card"
    bind="addCard"
    unbind="removeCard"
    cardinality="1..n"
    policy="dynamic"/>
</component>


This is what tells DS to call the Controller class when a game.model.UserInterface and a game.model.Card service is registered. It defines a component called 'controller', specifies its class and service dependencies. I've set the policy to 'dynamic' so I get notified every time one of these services gets bound or unbound. Matter of fact, this component configuration file actually causes my game.Controller class to be instantiated in the first place so that's why I don't need an OSGi Activator here. DS does it for me.
There's one last handy little nugget in here. You can see that the cardinality of the service dependencies is specified. For user interfaces it's 0 or more, but for cards it's 1 or more. The latter is handy, because it makes sure that my game Controller doesn't get created before there are a few cards registered. There's no point offering a card game to play without any cards!
So as soon as there's a few cards, my game.Controller class gets created and players can make themselves known by registering a UI.

We will need to tell DS about our OSGI-INF/controlcomponent.xml file. For this I have to reference it in the META-INF/MANIFEST.MF file. Mine looks like this:
  Bundle-Name: Duel Bundle
  Bundle-SymbolicName: Duel
  Bundle-Version: 1.0.0
  Import-Package: game.model
  Require-Bundle: org.eclipse.equinox.ds
  Service-Component: OSGI-INF/controlcomponent.xml


I won't go into too much detail on the game.GameImpl class. This is an implementation of the game.model.Game interface. It's not very complicated but it's just a bit too long for a blog post; so just look at the code :)
You can get the Game Implementation class with the Duel bundle from Subversion here: http://coderthoughts.googlecode.com/svn/trunk/duel/.

We can't start the game just yet, we first need to add a Bundle with a few cards. That's what the next posting is about.

2 comments:

bstarchev said...

Hi,
I have imported from repository projects: DuelInterfaces and Duel
But there are many errors like this in Duel/Controller.java:
The method run() of type new Runnable(){} must override a superclass method
What is the problem?
Boris Starchev, teacher Bulgaria

davidb said...

Hi Boris, this is a difference between Java 6 and Java 5. I developed the code using Java 6 and used @Override annotations for methods implementing an interface. However, Java 5 doesn't allow this.
I updated the code to be Java 5 compliant (removed those @Overrides). So just do an update from SVN and your bundles should compile without problems. Best regards, David