Component Implementation - Lesson 4 - Service Discovery

You component still shows in the service discovery list as an element with "Undefined description". It also doesn’t provide any interesting features or sub-nodes.

In this article I will show how to, in a simple way, change the basic component information presented on the service discovery list and how to add some service disco features. As a bit more advanced feature the guide will teach you about adding/removing service discovery nodes at run-time and about updating existing elements.

In order for the component to properly respond to disco#info and disco#items request you should register DiscoveryModule in your component:

@Override
protected void registerModules(Kernel kernel) {
	kernel.registerBean("disco").asClass(DiscoveryModule.class).exec();
}

NOTE It’s essential to explicitly register DiscoveryModule in your component.

Component description and category type can be changed by overriding two following methods:

@Override
public String getDiscoDescription() {
  return "Spam filtering";
}

@Override
public String getDiscoCategoryType() {
  return "spam";
}

Please note, there is no such 'spam' category type defined in the Service Discovery Identities registry. It has been used here as a demonstration only. Please refer to the Service Discovery Identities registry document for a list of categories and types and pick the one most suitable for you.

After you have added the two above methods and restarted the server with updated code, have a look at the service discovery window. You should see something like on the screenshot.

spam filtering disco small

Now let’s add method which will allow our module TestModule to return supported features. This way our component will automatically report features supported by all it’s modules. To do so we need to implement a method String[] getFeatures() which returns array of String items. This items are used to generate a list of features supported by component.

Although this was easy, this particular change doesn’t affect anything apart from just a visual appearance. Let’s get then to more advanced and more useful changes.

One of the limitations of methods above is that you can not update or change component information at run-time with these methods. They are called only once during initialization of a component when component service discovery information is created and prepared for later use. Sometimes, however it is useful to be able to change the service discovery during run-time.

In our simple spam filtering component let’s show how many messages have been checked out as part of the service discovery description string. Every time we receive a message we can to call:

updateServiceDiscoveryItem(getName(), null, getDiscoDescription() + ": [" + (++messagesCounter) + "]", true);

A small performance note, in some cases calling updateServiceDiscoveryItem(…​) might be an expensive operation so probably a better idea would be to call the method not every time we receive a message but maybe every 100 times or so.

The first parameter is the component JID presented on the service discovery list. However, Tigase server may work for many virtual hosts so the hostname part is added by the lower level functions and we only provide the component name here. The second parameter is the service discovery node which is usually 'null' for top level disco elements. Third is the item description (which is actually called 'name' in the disco specification). The last parameter specifies if the element is visible to administrators only.

spam filter counter small

The complete method code is presented below and the screenshot above shows how the element of the service discovery for our component can change if we apply our code and send a few messages to the component.

Using the method we can also add submodes to our component element. The XMPP service discovery really is not for showing application counters, but this case it is good enough to demonstrate the API available in Tigase so we continue with presenting our counters via service discovery. This time, instead of using 'null' as a node we put some meaningful texts as in example below:

// This is called whenever a message arrives
// to the component
updateServiceDiscoveryItem(getName(), "messages",
  "Messages processed: [" + (++messagesCounter) + "]", true);
// This is called every time the component detects
// spam message
updateServiceDiscoveryItem(getName(), "spam", "Spam caught: [" +
  (++totalSpamCounter) + "]", true);

Again, have a look at the full method body below for a complete code example. Now if we send a few messages to the component and some of them are spam (contain words recognized as spam) we can browse the service discovery of the server. Your service discovery should show a list similar to the one presented on the screenshot on the left.

Of course depending on the implementation, initially there might be no sub-nodes under our component element if we call the updateServiceDiscoveryItem(…​) method only when a message is processed. To make sure that sub-nodes of our component show from the very beginning you can call them in setProperties(…​) for the first time to populate the service discovery with initial sub-nodes.

Please note, the updateServiceDiscoveryItem(…​) method is used for adding a new item and updating existing one. There is a separate method though to remove the item:

void removeServiceDiscoveryItem(String jid,
  String node, String description)

Actually only two first parameters are important: the jid and the node which must correspond to the existing, previously created service discovery item.

There are two additional variants of the update method which give you more control over the service discovery item created. Items can be of different categories and types and can also present a set of features.

The simpler is a variant which sets a set of features for the updated service discovery item. There is a document describing existing, registered features. We are creating an example which is going to be a spam filter and there is no predefined feature for spam filtering but for purpose of this guide we can invent two feature identification strings and set it for our component. Let’s call update method with following parameters:

updateServiceDiscoveryItem(getName(), null, getDiscoDescription(),
  true, "tigase:x:spam-filter", "tigase:x:spam-reporting");

The best place to call this method is the setProperties(…​) method so our component gets a proper service discovery settings at startup time. We have set two features for the component disco: tigase:x:spam-filter and tigase:x:spam-reporting. This method accepts a variable set of arguments so we can pass to it as many features as we need or following Java spec we can just pass an array of Strings.

Update your code with call presented above, and restart the server. Have a look at the service discovery for the component now.

The last functionality might be not very useful for our case of the spam filtering component, but it is for many other cases like MUC or PubSub for which it is setting proper category and type for the service discovery item. There is a document listing all currently registered service discovery identities (categories and types). Again there is entry for spam filtering. Let’s use the automation category and spam-filter type and set it for our component:

updateServiceDiscoveryItem(getName(), null, getDiscoDescription(),
  "automation", "spam-filtering", true,
  "tigase:x:spam-filter", "tigase:x:spam-reporting");

Of course all these setting can be applied to any service discovery create or update, including sub-nodes. And here is a complete code of the component:

Example component code. 

public class TestComponent extends AbstractKernelBasedComponent {

  private static final Logger log = Logger.getLogger(TestComponent.class.getName());

  @Inject
  private TestModule testModule;

  @Override
  public synchronized void everyMinute() {
    super.everyMinute();
    testModule.everyMinute();
  }

  @Override
  public String getComponentVersion() {
  String version = this.getClass().getPackage().getImplementationVersion();
    return version == null ? "0.0.0" : version;
  }

  @Override
  public String getDiscoDescription() {
    return "Spam filtering";
  }

  @Override
  public String getDiscoCategoryType() {
      return "spam";
  }

  @Override
  public int hashCodeForPacket(Packet packet) {
    if (packet.getElemTo() != null) {
      return packet.getElemTo().hashCode();
    }
    // This should not happen, every packet must have a destination
    // address, but maybe our SPAM checker is used for checking
    // strange kind of packets too....
    if (packet.getStanzaFrom() != null) {
      return packet.getStanzaFrom().hashCode();
    }
    // If this really happens on your system you should look carefully
    // at packets arriving to your component and decide a better way
    // to calculate hashCode
    return 1;
  }

  @Override
  public boolean isDiscoNonAdmin() {
    return false;
  }

  @Override
  public int processingInThreads() {
    return Runtime.getRuntime().availableProcessors();
  }

  @Override
  public int processingOutThreads() {
    return Runtime.getRuntime().availableProcessors();
  }

  @Override
  protected void registerModules(Kernel kernel) {
    // here we need to register modules responsible for processing packets
    kernel.registerBean("disco").asClass(DiscoveryModule.class).exec();
  }

}

Example module code. 

@Bean(name = "test-module", parent = TestComponent.class, active = true)
public static class TestModule extends AbstractModule {

  private static final Logger log = Logger.getLogger(TestModule.class.getCanonicalName());

  private Criteria CRITERIA = ElementCriteria.name("message");
  private String[] FEATURES = { "tigase:x:spam-filter", "tigase:x:spam-reporting" };

  @ConfigField(desc = "Bad words", alias = "bad-words")
  private String[] badWords = {"word1", "word2", "word3"};
  @ConfigField(desc = "White listed addresses", alias = "white-list")
  private String[] whiteList = {"admin@localhost"};
  @ConfigField(desc = "Logged packet types", alias = "packet-types")
  private String[] packetTypes = {"message", "presence", "iq"};
  @ConfigField(desc = "Prefix", alias = "log-prepend")
  private String prependText = "Spam detected: ";
  @ConfigField(desc = "Secure logging", alias = "secure-logging")
  private boolean secureLogging = false;
  @ConfigField(desc = "Abuse notification address", alias = "abuse-address")
  private JID abuseAddress = JID.jidInstanceNS("abuse@locahost");
  @ConfigField(desc = "Frequency of notification", alias = "notification-frequency")
  private int notificationFrequency = 10;
  private int delayCounter = 0;
  private long spamCounter = 0;
  private long totalSpamCounter = 0;
  private long messagesCounter = 0;


  @Inject
  private TestComponent component;

  public void everyMinute() {
    if ((++delayCounter) >= notificationFrequency) {
      write(Message.getMessage(abuseAddress, component.getComponentId(), StanzaType.chat,
                               "Detected spam messages: " + spamCounter, "Spam counter", null,
                               component.newPacketId("spam-")));
      delayCounter = 0;
      spamCounter = 0;
    }
  }

  @Override
  public String[] getFeatures() {
    return FEATURES;
  }

  @Override
  public Criteria getModuleCriteria() {
    return CRITERIA;
  }

  public void setPacketTypes(String[] packetTypes) {
    this.packetTypes = packetTypes;
    Criteria crit = new Or();
    for (String packetType : packetTypes) {
      crit.add(ElementCriteria.name(packetType));
    }
    CRITERIA = crit;
  }

  @Override
  public void process(Packet packet) throws ComponentException, TigaseStringprepException {
    // Is this packet a message?
    if ("message" == packet.getElemName()) {
      component.updateServiceDiscoveryItem(component.getName(), "messages",
                                           "Messages processed: [" + (++messagesCounter) + "]", true);
      String from = packet.getStanzaFrom().toString();
      // Is sender on the whitelist?
      if (Arrays.binarySearch(whiteList, from) < 0) {
        // The sender is not on whitelist so let's check the content
        String body = packet.getElemCDataStaticStr(Message.MESSAGE_BODY_PATH);
        if (body != null && !body.isEmpty()) {
          body = body.toLowerCase();
          for (String word : badWords) {
            if (body.contains(word)) {
              log.finest(prependText + packet.toString(secureLogging));
              ++spamCounter;
              component.updateServiceDiscoveryItem(component.getName(), "spam", "Spam caught: [" +
                                                   (++totalSpamCounter) + "]", true);
              return;
            }
          }
        }
      }
    }
    // Not a SPAM, return it for further processing
    Packet result = packet.swapFromTo();
    write(result);
  }
}