Snippets / Extender aQute - Software Consultancy
Search
*

Extender Pattern with Automatic Servlet Registration

Purpose

The extender pattern allows other bundles to extend the functionality in a specific domain. For example, a PHP extender could find PHP programs in bundles, see the OSGi demoes. OSGi is quite unique that it allows bundles to react on the life cycle events of other bundles. The extender pattern uses this to create a model where application programmers can write small bundles that only contain the application information while the system programmers provide the extender bundles that read the application bundles and provide the right context. This snippet shows a skeleton of an extender; it looks at the ServletMap header in the bundle and if found, loads a servlet class from the bundle and registers it with the Http Service.

Prerequisites

Instructions

Our extender simplifies the life of servlet programmers. Instead of requiring them to create components that wait for the http service, register their servlet and do other chores, we only require them to wrap their servlet code in a bundle. The only cruft they have to provide is a header that specifies the name of the servlet as well as the class that should implement the servlet. Simple? Think so, "and look mammy, no XML!"

The only tricky aspect of the extender is the difference between bundles that already have been started and bundles that will be started in the near future. Already started bundles can be found through the Bundle Context getBundles() method. Bundles that will be started in the future can be captured with a Bundle Listener registered with the Bundle Context addBundleListener method. In this case we are not in a hurry so we can receive these bundle events asynchronously.

The tricky aspect is the synchronization between looking at the started bundles and receiving the events from newly started bundles. We solve this race condition by maintaining an activated set. Access to this set is carefully synchronized so that we do not register a started bundle twice.

Let us first design the header. We need to map the servlet alias (the name under which it is registered on the web) to the class name. Most OSGi headers permit multiple clauses so let us make the syntax as follows:

  ServletMap ::= map ( ',' map ) *
  map        ::= alias '=' classname
  alias      ::= <valid alias for the Http Servlet>
  classname  ::= <valid class name, loadable from the bundle>

When we discover that a bundle was or is started we call the activate(Bundle) method. This method first atomically decides if this bundle is already activated or not, if not, it just ignores the call otherwise it adds to the activated set. The synchronization is only around this check, which is good practice. It is very bad practice to call other function blocks, like the framework, while in a synchronized block because it makes deadlocks likely.

When a bundle is stopped, we nicely unregister the servlet. If we get deactivated all our registered aliases are unregistered. The unregistration happens in the deactivate(Bundle) method.

The Http Service provides a method registerServlet that must be given an alias (starting with /) under which the servlet is visible on the website, a servlet instance, configuration parameters (may be null) , and a Http Context instance. If the latter is null, a default context is used. In this case, we get the alias from the header.

We create our extender as a component that is statically bound to the Log Service and the Http Service. This means that if either is not available, our bundle will not run. We therefore do not have to worry about either of these services not being there. The Service Component Runtime calls the bind method setHttp and setLog before we are activated.

The first class we show is the Activator class. This class is a component and activated and deactivated by the Service Component Runtime. In this class we concentrate the OSGi specific details of activation, injection of the http and log dependencies, and deactivation. We also add the Bundle Listener here and call the appropriate methods in the Registry class, which is responsible for registering and unregistering the servlets.

  Source: aQute/extender/Activator.java
  package aQute.extender;

  import org.osgi.framework.*;
  import org.osgi.service.component.*;
  import org.osgi.service.http.*;
  import org.osgi.service.log.*;

  public class Activator implements BundleListener {
    BundleContext context;
    Registry      registry = new Registry();

    protected void activate(ComponentContext cc) {
      this.context = cc.getBundleContext();
      context.addBundleListener(this);
      Bundle bundles[] = context.getBundles();
      for (int i = 0; i < bundles.length; i++) {
        if ((bundles[i].getState() & 
            (Bundle.STARTING | Bundle.ACTIVE)) != 0) registry
            .register(bundles[i]);
      }
    }

    protected void deactivate(ComponentContext context)
        throws Exception {
      this.context.removeBundleListener(this);
      registry.close();
    }

    public void bundleChanged(BundleEvent event) {
      switch (event.getType()) {
      case BundleEvent.STARTED:
        registry.register(event.getBundle());
        break;

      case BundleEvent.STOPPED:
        registry.unregister(event.getBundle());
        break;
      }
    }

    public void setHttp(HttpService http) {
      registry.http = http;
    }

    public void setLog(LogService log) {
      registry.log = log;
    }
  }

The next class is the Registry class. The responsibility of this class is to register and unregister the servlets. The activated set maintains the list of bundles that have been activated. When a bundle is discovered, we check the ServletMap header. If present, we parse it in the getClauses(Bundle) method. For each clause, we register the appropriate servlet. When the bundle is stopped, we check the activated set to see if it was registered. If so, we parse the ServletMap header again and unregister all aliases.

  Source: aQute/extender/Registry.java
  package aQute.extender;

  import java.util.*;
  import javax.servlet.http.*;
  import org.osgi.framework.*;
  import org.osgi.service.http.*;
  import org.osgi.service.log.*;

  public class Registry {
    Set         activated = new HashSet();
    HttpService http;
    LogService  log;

    public void register(Bundle bundle) {
      synchronized (activated) {
        if (activated.contains(bundle)) return;
      }
      Map clauses = getClauses(bundle);

      for (Iterator e = clauses.entrySet().iterator(); e
          .hasNext();) {
        Map.Entry entry = (Map.Entry) e.next();
        try {
          String alias = (String) entry.getKey();
          String className = (String) entry.getValue();
          registerServlet(bundle, alias, className);
        } catch (Throwable t) {
          log.log(LogService.LOG_ERROR,
              "[extender] Activating servlet from "
                  + bundle.getLocation(), t);
        }
      }
      synchronized (activated) {
        activated.add(bundle);
      }
    }

    void registerServlet(Bundle bundle, String alias,
        String className) throws Exception {
      if (!alias.startsWith("/")) throw new IllegalArgumentException(
          "Alias must start with / : " + alias);

      Class clazz = bundle.loadClass(className);
      if (clazz != null) {
        HttpServlet servlet = (HttpServlet) clazz
            .newInstance();
        http.registerServlet(alias, servlet, null, null);
      } else throw new IllegalArgumentException(
          "Can not find class " + className);
    }

    public void unregister(Bundle bundle) {
      synchronized (activated) {
        if (!activated.contains(bundle)) return;
        activated.remove(bundle);
      }

      Map clauses = getClauses(bundle);
      for (Iterator i = clauses.keySet().iterator(); i
          .hasNext();) {
        String alias = (String) i.next();
        http.unregister(alias);
      }
    }

    Map getClauses(Bundle bundle) {
      Map map = new HashMap();
      String header = (String) bundle.getHeaders().get(
          "ServletMap");
      if (header != null) {
        String clauses[] = header.split(",");
        for (int i = 0; i < clauses.length; i++) {
          String parts[] = clauses[i].trim().split(
              "\\s*=\\s*");
          if (parts.length == 2) map.put(parts[0], parts[1]);
        }
      }
      return map;
    }

    void close() {
      for (Iterator i = activated.iterator(); i.hasNext();) {
        Bundle bundle = (Bundle) i.next();
        unregister(bundle);
      }
    }
  }

The extender has no external dependencies so we can keep the package private. We need to declare a service component, you can look at the HelloWorldComponent snippet to see how this header is created. That is all!

  Bnd file: aQute.extender.bnd
  Private-Package: aQute.extender
  Service-Component: aQute.extender.Activator; \ 
log=org.osgi.service.log.LogService; \
http=org.osgi.service.http.HttpService

If you run this code by doing Context Menu -> Make Bundle and drop the JAR file in the load directory then ... nothing happens. The reason is that we have no bundle installed with the ServletMap header set. So for testing reasons it is necessary to make a simple Hello World bundle with a servlet. We place this code in a sub-package of our extender: aQute.extender.hello.

  Source: aQute/extender/hello/HelloWorldServlet.java
  package aQute.extender.hello;

  import java.io.*;
  import javax.servlet.http.*;

  public class HelloWorldServlet extends HttpServlet {
    public void doGet(HttpServletRequest rq, 
        HttpServletResponse rsp ) throws IOException {
      PrintWriter pw = rsp.getWriter();
      pw.print("Hello World");
    }
  }

And of course a Bnd file that creates the JAR file:

  Bnd file: aQute.extender.hello.bnd
  Private-Package: aQute.extender.hello
  ServletMap = /hello=aQute.extender.hello.HelloWorldServlet

If you make and install this bundle, you can use your browser to go to http://localhost/hello (assuming your browser runs on port 80). This should show: "Hello World".

Links

Copyright 2006 aQute SARL, All Rights Reserved