Advance Event & Query Configuration Tutorial

In this tutorial we will create one publisher and two listeners. The publisher will publish current temperature in celsius. The first listener will receive each temperature event and print out on standard output the values of temperature in celsius. The second listener will only receive the temperature events that are below zero degrees celsius.

This tutorial will explain:

  • Defining a event with namespace and XML Schema.
  • Defining a query with logical criteria.

The preferred way of completing this tutorial is to download the skeleton project and use it to implement functionality of this tutorial. For impatient ones, whole source code of the completed tutorial can be downloaded from here.

Table of content:

Publisher & Listener Top

First step is to define an ETemperature event and create publisher and listener that receives all ETemperature events. We start by defining the ETemeprature event in the publishers.cfg.xml file.

<Context xmlns="http://www.sm4all-project.eu/COPAL">
  <Event name="ETemperature" />
</Context>

The publisher will simulate a temperature sensor by publishing a random temperature between -20 and 35 °C every 5 seconds. Its structure is similar to the EHello publisher we created in the Hello World tutorial:

public class TemperatureSensor extends BasePublisher {

    private final Random random = new Random(System.currentTimeMillis());
    private Timer timer = null;

    public TemperatureSensor() {
        super("TemperatureSensor", "ETemperature");
    }

    @Override
    protected boolean start(final ContextEventType type) {
        final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        factory.setNamespaceAware(true);

        try {
            final DocumentBuilder builder = factory.newDocumentBuilder();
            final Document event = ((XMLEventType) type).createEvent(builder);

            this.timer = new Timer();
            this.timer.schedule(new TimerTask() {

                @Override
                public void run() {
                    // temperature in Celsius is between -20 and 35.
                    final int temperature = TemperatureSensor.this.random.nextInt(56) - 20;
                    event.getDocumentElement().setAttribute("celsius", String.valueOf(temperature));

                    try {
                        publish(new XMLEvent((XMLEventType) type, getSourceID(), event));
                    } catch (final ContextException ex) {
                        System.out.println("Something went wrong");
                        ex.printStackTrace();
                    }
                }
            }, 0, 5000);

            return true;
        } catch (final ParserConfigurationException ex) {
            return false;
        }
    }

    @Override
    protected void stop(final ContextEventType eventType) {
        this.timer.cancel();
    }
}

When the publisher is started, it creates an ETemperature XML Document using the createEvent(DocumentBuilder) method in the XMLEventType class. Remember that a XML Document is cloned before publishing, therefore we can reuse the XML Document that we created at the beginning and just update the celsius attribute whenever we want to publish new temperature.

The OSGi Activator for the bundle looks identical to the publisher's Activator in the Hello World tutorial:

public class Activator extends PublishersActivator {

    @Override
    protected void start() {
        register(new TemperatureSensor());
    }
}

The second bundle will hold a listener that will receive the published ETemperature events. The listener just prints the value of the celsius attribute on the standard output:

import at.ac.tuwien.infosys.sm4all.copal.api.listener.Events;
import at.ac.tuwien.infosys.sm4all.copal.api.listener.Event;
import at.ac.tuwien.infosys.sm4all.copal.api.util.Name;

@Name("TemperatureListener")
public class TemperatureListener {

    @Event(type = "ETemperature")
    public void onTemperature(final XMLEvent event) {
        final Document document = event.getDocument();
        final String temperature = document.getDocumentElement().getAttribute("celsius");

        System.out.println("New temperature");
        System.out.println("Celsius: " + temperature);
    }
}

The difference between EHello listener developed in the Hello World tutorial and this one is that we not extending a BaseListener and instead are using the annotations to define the listener. First the class must be annotated with the @Name annotation that specifies the name of the listener and it must have at least one method annotated with the @Event or @Events annotation. The @Event annotation can optionally specify which type of event the method should be invoked with an when type is not used than the method is invoked with any event type. The @Events annotation is just a composition annotation when you need to annotate the method with multiple @Event annotations and it is not used in this example.

The rules for method to be annotated with @Event or @Events annotation are:

  • the method must be public,
  • the method must have exactly one parameter,
  • the parameter must be ContextEvent or a subclass (in our example it is the XMLEvent).

The rules, for which methods are invoked when an event is received, are:

  • the event must have class equal to or a subclass of the method parameter,
  • if the method is annotated with the @Event annotation that specifies a type than the event must be of that type,
  • if the method is annotated with the @Event annotation that does not specify a type than the event can be of any type,
  • if the method is annotated with the @Events annotation than the event must match any @Event annotation.
  • all methods for which above rules hold will be invoked with the event.

Example of a method that catches all events (because we don't specify any type in the @Event annotation and the parameter is ContextEvent that is the superclass of all events) would be:

@Event
public void onAnyEvent(final ContextEvent event) {
    //do something
}

Finally we define the query for the ETemperature events in the listeners.cfg.xml file and register the listener to it using the OSGi Activator. The listener.cfg.xml file looks like:

<Context xmlns="http://www.sm4all-project.eu/COPAL">
  <Query name="ETemperature.All" event="ETemperature" />
</Context>

And the activator should look like (do not forget also to set bundle.activator property in the listeners subproject's pom.xml file). Notice that we must wrap the TemperatureListener with the AnnotatedListener because it doesn't implement the ContextListener interface:

import at.ac.tuwien.infosys.sm4all.copal.api.listener.AnnotatedListener;

public class Activator extends ListenersActivator {

    @Override
    protected void start() {
        register("ETemperature.All", new AnnotatedListener(new TemperatureListener()));
    }
}

If we run the example now, we would see something similar to this on the standard output:

New temperature
Celsius: 28
New temperature
Celsius: 0
New temperature
Celsius: -4
...

Namespace Top

Next step is to put the ETemperature into its own namespace. The configuration of the namespace for an event is not mandatory, but having events in their own namespaces has advantage like preventing name clashes of the root elements between events.

Adding a namespace to the ETemperature event is very simple. In the publishers.cfg.xml we add a namespace attribute to the ETemperature's Event element which specified the event's default namespace. Only consequence is that your root element must be in this namespace; the root's child element do not have to be in same namespace, but it is preferable.

<Context xmlns="http://www.sm4all-project.eu/COPAL">
  <Event name="ETemperature" namespace="http://www.sm4all-project.eu/COPAL/Tutorial" />
</Context>

Only difference in the publisher is that DocumentBuilderFactory must be aware of namespaces. Here we can see the advantage of using the createEvent(DocumentBuilder) method, because it automatically will define the default namespace of the created XML document to be the event's namespace and use it when creating the root element.

@Override
protected boolean start(final ContextEventType type) {
    final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
    factory.setNamespaceAware(true);

    try {
        final DocumentBuilder builder = factory.newDocumentBuilder();
        final Document event = ((XMLEventType) type).createEvent(builder);

        ...

        return true;
    } catch (final ParserConfigurationException ex) {
        return false;
    }
}

There should be no change in the output if we run the example now. The advantage when we implement publishers and listeners in this way is that we can change the event's namespace in the publishers.cfg.xml and we do not have to recompile the example. We can even remove the namespace configuration and it should still work. This also holds for changing the name of the root element, because we used the createEvent(DocumentBuilder) method; it would just create the XML Document with new name for the root element. You should play with the definition of the ETemperature event, changing the name of the root element and event's namespace, and the example should always work without touching the implementation of the publisher and the listener.

Schema Top

The last configuration for events we can use is to define the XML Schema for the ETemperature event. First we have to create the XML Schema file that defines the ETemperature XML element and we put it into the publishers subproject src/main/resource directory and name it ETemperature.xsd:

<xsi:schema
  xmlns="http://www.sm4all-project.eu/COPAL/Tutorial"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema"
  targetNamespace="http://www.sm4all-project.eu/COPAL/Tutorial"
  elementFormDefault="qualified">

  <xsi:element name="ETemperature">
    <xsi:complexType>
      <xsi:attribute name="celsius" type="xsi:decimal" use="required" />
    </xsi:complexType>
  </xsi:element>
</xsi:schema>

If you are familiar with the XML Schema you will see that this schema file is simple. We define that the root element has name ETemperature and it has one and only one attribute named celsius which is of decimal type. Root elements is in its own namespace: http://www.sm4all-project.eu/COPAL/Tutorial.

In the publishers.cfg.xml we have to tell the location of this XML Schema file for the ETemperature event. We do this with Schema element which has one of these children: URL or ClassPath. The URL is used when we have an URL location for the schema file like a web address. The ClassPath contains the name of the schema file that can be located in the bundle's classpath and can be read using standard Java class loading facilities. Both URL and Classpath elements require a location argument which respectively specifies URL and classpath location where to look for the XML Schema file. In our example, we have put the ETemperatude.xsd file into the publishers subproject, and we need to use the Schema with ClassPath in the publishers.cfg.xml:

<Context xmlns="http://www.sm4all-project.eu/COPAL">
  <Event name="ETemperature" namespace="http://www.sm4all-project.eu/COPAL/Tutorial">
    <Schema>
      <ClassPath location="ETemperature.xsd" />
    </Schema>
  </Event>
</Context>

If you rerun the example, you should see the same output as before.

Logical Criteria Top

You will probably wonder what is advantage of defining XML Schema files for our event. Basically, it helps COPAL know about the structure of your events and their types and provides the facility to create more advance queries using logical criteria to select events that listeners are interested in. In our example, we can now use the celsius attribute to only receive temperature events that are below zero degrees celsius. This is all possible because we defined that the ETemperature event has celsius attribute that is of decimal type.

First let us create a simple listener for our minus temperature events that prints out a message on the standard output just to show that it works:

public class MinusTemperatureListener extends BaseListener {

    public MinusTemperatureListener() {
        super("MinusTemperatureListener");
    }

    @Override
    public void onEvent(final ContextEvent event) {
        System.out.println("Too cold!");
    }
}

We can now define the Below zero degrees celsius ETemperature query in the listeners.cfg.xml and register this listener with it same way we registered the TemperatureListener with the ETemperature.All query and it will work, but we will create a query and register this listener programmatically to show that you can also define queries and register listeners dynamically inside bundles at runtime. You do not need to use the listeners.cfg.xml file at all, but you should because it hides a lot of boilerplate code as we will see now. Just for the sake of demonstration the configuration of the ETemperature.BelowZero query in the listeners.cfg.xml would look like and notice that less-than sign has to be escaped for the listeners.cfx.xml to be valid XML file:

Query name="ETemperature.BelowZero" event="ETemperature" criteria="celsius &lt; 0" />

The dynamic way of creating queries is to override the ListenersActivator's start(ContextQueryFactory) method and use the provided factory to create queries. Both start methods can be overridden simultaneously as we will see in our example. This start method is called when an OSGi service is registered which implements the ContextQueryFactory interface and it comes in a pair with the stop() method that is called when the ContextQueryFactory service becomes unavailable and should be used to destroy all queries that we created in the start method.

The listeners' activator now looks like:

public class Activator extends ListenersActivator {

    private ProcessedEventQuery belowZeroQuery;

    @Override
    protected void start() {
        register("ETemperature.All", new AnnotatedListener(new TemperatureListener()));
    }

    @Override
    protected void start(final ContextQueryFactory queryFactory) {
        try {
            this.belowZeroQuery = queryFactory.create("ETemperature.BelowZero",
                    "ETemperature", "celsius < 0");
            this.belowZeroQuery.register(new MinusTemperatureListener());
        } catch (final RedefinitionOfQueryException ex) {
            System.out.println("ETemperature.BelowZero query is already defined with different event or criteria");
        } catch (final AlreadyRegisteredException ex) {
            System.out.println("Listener with same name is already registered with ETemperature.BelowZero query");
        } catch (final QueryDestroyedException ex) {
            System.out.println("Below zero ETemperature query is unexpectedly destroyed");
        }
    }

    @Override
    protected void stop() {
        if (this.belowZeroQuery != null)
            try {
                this.belowZeroQuery.destroy();
            } catch (final QueryDestroyedException ex) {
                System.out.println("Below zero ETemperature query is already destroyed");
            } finally {
                this.belowZeroQuery = null;
            }
    }
}

Now you can see why it is so much easier define queries in the listeners.cfg.xml file and use the start() method to register listener.

The first line in the try-catch block of the start method creates the query with name ETemperature.BelowZero that receives ETemperature events that have celsius attribute less than 0. The create method can throw a RedefinitionOfQueryException if there is already a query with same name that receives different events or has different logical criteria, therefore, it is meaningful to put the name of the event in the name of the query, as we have done in all queries that we have previously defined. This ensures that if we create another temperature event with different name, the queries for the second temperature event would not clash with queries for the ETemperature event. The second line registers the listener with the query. The registration can throw AlreadyRegisteredException or QueryDestroyedException. The AlreadyRegisteredException is thrown when there is a listener with same name already registered with the query, and the QueryDestroyedException is thrown when the query has been previously destroyed.

The first line in the try-catch block of the stop method destroys the query. It can throw also the QueryDestroyedException which in this case can be ignored, because it was already our intention to destroy the query.

When we run the example, we can see that the "Too cold" message is only printed when a temperature is below zero:

New temperature
Celsius: 11
New temperature
Celsius: -11
Too cold
New temperature
Celsius: 32
New temperature
Celsius: -5
Too cold
...

This finishes the tutorial that explains the more advance configurations for the events and queries.