Chapter 8. Event-driven Applications with EventAdmin

The OSGi EventAdmin service provides a means to publish and receive events between bundles. This can be used to build applications that dynamically react to changes from external or internal sources.

This chapter will present the OSGi EventAdmin service and how it can be used to build decoupled applications. The EventAdmin service is an example of the whiteboard pattern, and therefore provides a means to loosely couple the components together.

Understanding the OSGi EventAdmin service

The OSGi EventAdmin service is described in the OSGi Compendium and OSGi Enterprise specifications in Chapter 113, Event Admin Service Specification. It provides a means to use a publish and subscribe mechanism to send events that may be targeted at a particular topic and may contain an arbitrary number of event properties.

Event topics are text names that are used to identify where the event will be delivered. They are represented with a slash (/) separating parts of the name, for example, org/osgi/framework/ServiceEvent or org/osgi/service/log/LogEntry.

An Event is an immutable object, initialized from a Dictionary or Map, which has a number of properties. These properties can store user-specific data, along with a number of other (potentially null) standard properties from the EventConstants class:

  • BUNDLE_IDbundle.id, the bundle's ID number
  • BUNDLE_SIGNERbundle.signer, the name of the signer of the bundle
  • BUNDLE_SYMBOLICNAMEbundle.symbolicName, the symbolic name of the bundle
  • BUNDLE_VERSIONbundle.version, the bundle's Version number
  • EXCEPTIONexception, a Throwable object if the event was raised as an error
  • EXCEPTION_CLASSexception.class, the class name of the exception object (useful for filtering out types of exceptions such as NullPointerException)
  • EXCEPTION_MESSAGEexception.message, the message returned from the exception object if present
  • MESSAGEmessage, a human-readable message that is usually not localized
  • SERVICE_IDservice.id, the ID of the service that generated the event
  • SERVICE_OBJECTCLASSservice.objectClass, the class name of the service that generated the event (suitable for matching/filtering)
  • SERVICE_PIDservice.pid, the persistent identifier of the service that raised the event
  • TIMESTAMPtimestamp, the time that the event was posted

Sending e-mails

The example in the next few sections will cover the case of components needing to send e-mails. A component may desire to send an e-mail when a particular condition occurs, such as if an error is received when processing an event, or if a user has feedback to submit.

Clearly, the component can be directly linked to an SMTP library such as commons-email. This requires some configuration in the component such as what the SMTP hostname is, what ports should be used, and whether there are any additional authentication details required.

Cleanly separating the e-mail generator (the component wishing to report an error) from the component that sends the e-mail is desirable, as this provides a loosely coupled system, as described in the Loosely coupled and highly cohesive section in Chapter 7, Designing Modular Applications. The configuration data of the generator component itself does not need to worry about how the e-mail is transmitted, while the component that sends the mail can be configured appropriately (or updated when the e-mail requirements are updated).

Although this can be implemented as an OSGi service, using the EventAdmin service allows the event to be handed off into the background, thereby not blocking the call site. The mail can also be created using properties in the Event object so that the clients can add whatever information is required in order to generate the e-mails.

Creating an event

The org.osgi.service.event.Event object is a standard class in the OSGi framework. It can be constructed from either a Map or Dictionary of standard key/value pairs, along with a topic (the intended destination of the event).

As e-mails have an Importance field, the topic name can be used to distinguish between low, normal, and high priority items with smtp/low, smtp/normal, and smtp/high. The e-mail's content can be stored as event properties using standard RFC 822 header names (To, From, and Subject):

import org.osgi.service.event.Event;
… 
Map<String,String> email = new HashMap<String, String>();
email.put("Subject","Hello World");
email.put("From","[email protected]");
email.put("To","[email protected]");
email.put("Body","Sample email sent via event");
Event event = new Event("smtp/high",email);

When this event is created, a copy of the email map is made so that subsequent changes to the email map are not reflected in the event object.

Tip

If the map is complex, and it needs to be used for multiple events, consider converting it to an EventProperties object. This will still perform a one-off copy (at construction time) but the EventProperties object can be reused as is for multiple events.

Posting an event

The EventAdmin service is used to deliver events synchronously or asynchronously.

Synchronous delivery can be performed with the sendEvent method. The event may be posted on the same thread that the client code is running on, but will be blocked until the event has been delivered to all the registered listeners. This is useful when events require processing before they are committed, but using this pattern can lead to deadlock in the system. For example, if the posting thread has a lock on a critical section, and the event listener also requires that critical lock, then the listener may never receive that notification. Once a system is deadlocked, then it cannot make further progress.

Tip

Avoid using synchronous delivery, as it can lead to deadlock in a system.

Asynchronous delivery can be performed with the postEvent method. The EventAdmin service enqueues the event and then calls it back with a different thread than the caller. The asynchronous delivery is more performant than the synchronous one, because it needs to use less locking and internal bookkeeping to determine which listeners have seen which events.

Sending an event requires acquiring an EventAdmin instance followed by the sendEvent or postEvent methods. The EventAdmin instance can be injected via Declarative Services or acquired through the BundleContext via FrameworkUtil if necessary (see Chapter 3, Using OSGi Services to Dynamically Wire Applications, for more details on how to acquire services in this way):

EventAdmin eventAdmin = getService(EventAdmin.class);
eventAdmin.postEvent(event);

The event will be queued and delivered to the listeners at some point later. Listeners that subscribe after the event is posted/sent will not see that event, though they will see future events.

Note

Note that there are no guarantees that the message will be sent; unlike an enterprise message queuing system, the message is not persisted to disk or replayed upon restart. It is stored in-memory only, and will be discarded when the JVM shuts down.

Additionally, there is no guarantee that a listener is present at the time an event is sent; it may just be silently discarded. EventAdmin does not guarantee consistency like a two-phase commit message store.

Receiving an event

EventAdmin is used to manage a set of listeners for particular events. Each event listener implements the EventHandler interface. However, instead of having an addListener such as the Observable class, EventAdmin looks for services published under the EventHandler interface. This allows listeners to come and go or be replaced with alternative (or mock) ones. These can be registered with all the usual service properties (such as service.ranking) or with Declarative Services:

package com.packtpub.e4.advanced.event.mailman;
import org.apache.commons.mail.*;
import org.osgi.service.event.*;
import org.osgi.service.log.*;
public class MailSender implements EventHandler { 
  public void handleEvent(Event event) {
    String topic = event.getTopic();
    if (topic.startsWith("smtp/")) {
      String importance = topic.substring("smtp/".length());
      String to = (String) event.getProperty("To");
      String from = (String) event.getProperty("From");
      String subject = (String) event.getProperty("Subject");
      String body = (String) event.getProperty("DATA");
      try {
        Email email = new SimpleEmail();
        email.setHostName(hostname);
        email.setSmtpPort(port);
        email.setFrom(from);
        email.addTo(to);
        email.setSubject(subject);
        email.addHeader("Importance",importance);
        email.setMsg(body);
        email.send();
        log(LogService.LOG_INFO, "Message sent to " + to);
      } catch (EmailException e) {
        log(LogService.LOG_ERROR, "Error occurred" + e);
      }
    }
  }
  private void log(int level, String message) {
    LogService log = this.log;
    if (log != null) {
      log.log(level, message);
    }
  }
}

Tip

The code sample uses commons-email from the Apache project and javax.mail:mail, both of which are available at Maven Central. Copies of the JARs are also available in the book's GitHub repository at https://github.com/alblue/com.packtpub.e4.advanced/.

When an event is received, if the topic begins with smtp/, then the event is converted into an Email object and sent.

The rest of the topic is used as a field for the Importance value, described in RFC 4021, and can take the values low, normal, and high (this is a hint and is not displayed by all e-mail clients).

The log method is a simple wrapper around a LogService, which logs if the service is available and not otherwise.

Tip

The log is captured in a local variable to avoid threading issues; if the log were unset during the call, then it would result in a NullPointerException between the if test and the call. Instead, by capturing it in a local variable once, the log can never be null in the if block.

To allow the handler to receive events, it needs to be registered as a service with the event.topics service property. The topics can be specified with an exact name (so that different handlers can be used to send smtp/low and smtp/high), but it's also possible to use a wildcard at the end of the topic name to pick up all the events (such as smtp/*).

If registering the handler manually (such as in an Activator), it would look like the following:

Dictionary<String, String> properties 
 = new Hashtable<String, String>();
  properties.put("event.topics","smtp/*");
  context.registerService(EventHandler.class, new MailSender(),
   properties);

However, it would be much cleaner to create a Declarative Services component to register the EventHandler; this will stay on standby and be created when first used, along with providing an easy way to acquire the LogService:

<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0"
 name="com.packtpub.e4.advanced.event.mailman.mailman">
  <implementation
   class="com.packtpub.e4.advanced.event.mailman.MailSender"/>
  <service>
    <provide interface="org.osgi.service.event.EventHandler"/>
  </service>
  <reference bind="setLogService" cardinality="0..1"
   interface="org.osgi.service.log.LogService" name="LogService"
   policy="dynamic"/>
  <property name="event.topics" value="smtp/*"/>
</scr:component>

Another benefit of using Declarative Services is the ability to pick up the configuration information from Config Admin to set the various properties on the object, such as the hostname to send mails to and the from address of the sender.

Filtering events

The EventAdmin service provides a coarse way to filter events through the use of topics. The topic name is a path with segments separated by a slash /, which can either be exact, or with the last segment as a wildcard, such as smtp/*, as shown in the previous example.

However, this does not provide a means to filter specific events. If additional filtering is desired (for example, ensuring that only select mails are processed), then this would traditionally be done in the code:

if(to.equals("[email protected]")) {
  …
}

Such hard-coded tests are difficult to debug or modify in the future.

The EventAdmin service provides a standardized way to filter events before they are delivered to the EventHandler. This allows more specific filters to be set as a way of reducing the volume of events that are handled for processing.

The filter is set by adding a service property event.filter, which contains an LDAP style expression. The expression can refer to any of the properties set in the event.

To add a filter equivalent to the previous e-mail filter, set the following to the Declarative Services component when registering the filter:

<property name="event.topics" value="smtp/*"/>
<property name="event.filter" value="(Subject=Hello World)"/>

Note

If the value contains special characters including parentheses () or asterisk *, then they must be prefixed with an escape character, which is the backslash character. The LDAP filter semantics are defined in RFC 1960.

Now only e-mails with a specific subject will be matched. Other LDAP filters can be used including wildcards to match substrings. For example, to match the local domain, (To=*@localhost) could be used.

Tip

Using the event.filter is usually more efficient than hardcoding the logic in the handleEvent method, because the LDAP filter is optimized in OSGi and is translated to a highly performant match when the handler is installed.

Secondly, when using Declarative Services, the component is only enabled when a matching event is seen; if events that don't match the filter are received, then the component isn't enabled. This allows the system to start up quickly without needing to enable components until they are first needed.

Threading and ordering of event delivery

The EventAdmin specification ensures that events posted asynchronously will be delivered on a different thread than the one that posts it. It does not guarantee that a particular thread will be used for all the events; nor does it guarantee that for synchronous event delivery, the same thread will be used. Implementations are free to either reuse the delivery thread for synchronous events, or use a different thread, provided that the blocking/waiting synchronization primitives are obeyed.

What the EventAdmin specification does guarantee is that events delivered to a particular topic are delivered in-order to individual subscribers. This implies that for each topic there is a queue for delivery to subscribers. Some implementations use a single thread to enforce ordered event delivery.

If asynchronous event delivery does not need to be strictly in-order, there is an event.delivery property that can take the value async.unordered (or referenced via the constant DELIVERY_ASYNC_UNORDERED in the EventConstants type). If the strict ordered requirement is relaxed, the EventAdmin implementation may be able to take advantage of multiple threads for event delivery.

Apache Felix provides a configuration option that can control the number of threads that are used for the EventAdmin delivery process. The default size for the Felix EventAdmin is 10, specified in org.apache.felix.eventadmin.ThreadPoolSize. The Equinox implementation (as of Eclipse 4.4) does not support this property, nor does it support multiple threads for delivery to event clients.

Note

The limitation of threading in the EventAdmin means that it may not be suitable for high-performance applications or one where rapid or timely event delivery is required. For high-performing message delivery, using Reactive Java (also known as RxJava) may be more appropriate.

Comparison between EventAdmin and services

Another approach to sending e-mails would be to use an e-mail Service, with a sendEmail method taking a Dictionary or Map object. This would do the same thing as the handleEvent method in the MailSender class.

There are a few differences that are worth covering when deciding whether to use events or services:

  • Synchronicity: The OSGi services approach will always be synchronous; the call will block until the recipient has processed the request and returned. On the other hand, event-based processing can either be synchronous or asynchronous depending on the client's calling convention.
  • Cardinality: The OSGi services approach will (typically) return a single service to interact with, whereas the event-based mechanism is a broadcast to all listeners. It requires no extra code effort for the event-based processing to add an additional listener to process messages, whereas with the OSGi services, the client has to be explicitly coded to deal with multiple services.
  • Typed: The OSGi services model allows a custom service for each type of action, using a number of different arguments with specified types. Clients are obviously compiled with those services in place and are therefore strictly typed. The event model uses the same interface for everything (Event) and essentially stores arbitrary untyped payloads.
  • Interface/Topic: The OSGi services model is based entirely on an interface type, which means that clients need to know the interface in advance. The interface can also be (semantically) versioned to enable future growth. The event-based mechanism is topic based and so the topic name needs to be known in advance. The topic allows for wildcarding, so an event published to com/example/event/bus will be picked up by listeners for com/example/event/* as well as com/example/* or even com/*. These events could be mapped into other communication technologies such as JSON messages over WebSocket to a browser, or even hooked up to a traditional JMS-based messaging system.

There isn't a hard rule as to when it's appropriate to use services or topics. Because topics can be created on the fly, it can be useful to represent events being fired for individual data objects to notify listeners that a change has happened; in fact, this technique is used in E4 to notify view components when the underlying model has changed as well as when the selection has changed.

In general, for broadcast mechanisms and topics that need to be created on the fly, the event mechanisms may be more suitable. For library and functional services, especially where a return value or state is needed, OSGi services may be more appropriate.

Framework events

The OSGi framework also has a number of events that get fired to notify subscribers when state changes occur in the framework itself. For example, when a bundle transitions through its life cycle states (uninstalled, installed, resolved, starting, active, and stopping), an event will be sent to notify clients of the new value. The events are sent after the transition has occurred.

The framework also has events that represent the system as a whole (to indicate the system has started, packages are refreshed, and for startlevel modifications) as well as certain log message types (info, warning, and error).

Framework events are sent under the org/osgi/framework/FrameworkEvent/ topic prefix and include:

  • STARTED
  • PACKAGES_REFRESHED
  • STARTLEVEL_CHANGED
  • INFO
  • WARNING
  • ERROR

Bundle events are sent under the org/osgi/framework/BundleEvent/ topic prefix and include:

  • UNINSTALLED
  • INSTALLED
  • UNRESOLVED
  • RESOLVED
  • STARTED
  • UPDATED
  • STOPPED

Service events are sent under the org/osgi/framework/ServiceEvent/ topic prefix and include:

  • REGISTERED
  • MODIFIED
  • UNREGISTERING

For framework events, the following information will be provided:

  • event: The FrameworkEvent object sent to the framework listeners

For bundle events, the following information will be provided:

  • event: The BundleEvent object sent to bundle listeners
  • bundle: The Bundle object
  • bundle.id: The source's bundle ID as a Long
  • bundle.signer: The string or collection of strings containing the distinguished name of the bundle's signers, if signed
  • bundle.symbolicName: The bundle's symbolic name if set
  • bundle.version: The bundle version as a Version

For service events, the following information will be provided:

  • event: The ServiceEvent object sent to the service listeners
  • service: The ServiceReference of the service
  • service.id: The service's ID as a Long
  • service.objectClass: The array of strings of the object classes this service is registered against
  • service.pid: The service's persistent identifier as a string or collection of strings

Along with delivery via EventAdmin, the framework supports several custom listeners that can be added via the BundleContext add listener methods:

  • FrameworkListener: This is an interface to receive FrameworkEvent objects
  • BundleListener: This is an interface to receive BundleEvent objects
  • ServiceListener: This is an interface to receive ServiceEvent objects

Note that classes such as ServiceTracker (covered in Chapter 3, Using OSGi Services to Dynamically Wire Applications) subscribe to service events in order to determine when services come and go, and extender patterns such as Declarative Services listen to bundle events in order to process them when they are installed or removed. Implementing an extender pattern generally looks like the following:

context.addBundleListener(new BundleListener() {
  public void bundleChanged(BundleEvent event) {
    int type = event.getType();
    Bundle bundle = event.getBundle();
    String myHeader = bundle.getHeaders().get("X-MyHeader");
    if (type == BundleEvent.STARTED && myHeader != null) {
      addBundle(bundle);
    } else if (type == BundleEvent.STOPPED && myHeader != null) {
      removeBundle(bundle);
    }
  }
});
for (Bundle bundle : context.getBundles()) {
  if (bundle.getState() == Bundle.ACTIVE
    && bundle.getHeaders().get("X-MyHeader") != null) {
    addBundle(bundle);
  }
}

Note that the pattern is generally to add the bundle listener first, and then iterate through all the existing bundles. This way, there may be some duplicate calls to the addBundle method, but the bundles should not be missed. Generally, the extender pattern will maintain a list of bundles under management so that when the extender provider is stopped, the bundles can be appropriately released.

Also note that most patterns use the existence of a header in the manifest to determine whether or not the bundle should be extended. This allows for a cheap test to determine if the bundle has any contents, and also why headers such as Service-Component exist in the Declarative Services specification.

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

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