Component implementation - Lesson 2 - Configuration

It might be hard to tell what is the first important thing to do with your new component implementation. Different developers may have a different view on this. It seems to me however that it is always a good idea to give to your component a way to configure it and provide some runtime settings.

This guide describes how to add configuration handling to your component. There is detailed configuration API description available so again I am not getting deep into all details just the necessary code.

To demonstrate how to maintain the component configuration let's say we want to make configurable types of packets which are being logged by the component. There are three possible packet types: 'message', 'presence' and 'iq' and we want to be able to configure logging of any combination of them. Furthermore we also want to be able to configure the text which is prepended to the logged message and optionally switch the secure logging on. (Secure logging replaces all packet CData with text: 'CData size: NN' to protect user privacy.)

Let's create following private variables in our component:

private String[] packetTypes = {"message", "presence", "iq"};
private String prependText = "My packet: ";
private boolean secureLogging = false;

As the component configuration is maintained in a form of (key, value) Map we have to invent keys for each of our configuration entry:

private static final String PACKET_TYPES_KEY = "packet-types";
private static final String PREPEND_TEXT_KEY = "log-prepend";
private static final String SECURE_LOGGING_KEY = "secure-logging";

There are two methods used to maintain the component configuration: getDefaults(...) where the component provides some configuration defaults and setProperties(...) which sets working configuration for the component:

@Override
public Map<String, Object> getDefaults(Map<String, Object> params) {
  Map<String, Object> defs = super.getDefaults(params);
  defs.put(PACKET_TYPES_KEY, packetTypes);
  defs.put(PREPEND_TEXT_KEY, prependText);
  defs.put(SECURE_LOGGING_KEY, secureLogging);
  return defs;
}
 
@Override
public void setProperties(Map<String, Object> props) {
  super.setProperties(props);
  packetTypes = (String[])props.get(PACKET_TYPES_KEY);
  prependText = (String)props.get(PREPEND_TEXT_KEY);
  secureLogging = (Boolean)props.get(SECURE_LOGGING_KEY);
}

You do not have to implement getDefaults(...) method and provide default settings for your configuration but doing so gives you a few benefits.

The first, from the developer point of view, you don't have to check in the setProperties(...) whether the value is of a correct type or convert it from String to the correct type as it always be either the default or user provided. It will be of a correct type as the configuration framework takes care of the types comparing between the user provided settings and default values. So this just makes your setProperties(...) code much simpler and clearer.

Secondly this also makes the administrator live easier. As you can see on the screenshot, configuration parameters provided with default values, can be changed via configuration ad-hoc commands. So the administrator can maintain your component at run-time from his XMPP client.

Regardless you implemented the getDefaults(...) method or not you can always manually add parameters to the init.properties file.

The syntax in init.properties file is actually very simple and is described in details in the documentation for this file. As it shows on the screenshot the configuration parameter name consists of: 'component name' + /  + 'property key'. To set configuration for your component in init.properties file you have to append following lines to the file:

test/log-prepend="My packet: "
test/packet-types[s]=message,presence,iq
test/secure-logging[B]=true

In square brackets you provide the property type, have a look at the documentation for more details.

And this is the complete code of the new component with modified processPacket(...) method taking advantage of configuration settings:

import java.util.Map;
import java.util.logging.Logger;
import tigase.server.AbstractMessageReceiver;
import tigase.server.Packet;
 
public class TestComponent extends AbstractMessageReceiver {
 
  private static final Logger log =
    Logger.getLogger(TestComponent.class.getName());
 
  private static final String PACKET_TYPES_KEY = "packet-types";
  private static final String PREPEND_TEXT_KEY = "log-prepend";
  private static final String SECURE_LOGGING_KEY = "secure-logging";
 
  private String[] packetTypes = {"message", "presence", "iq"};
  private String prependText = "My packet: ";
  private boolean secureLogging = false;
 
  @Override
  public void processPacket(Packet packet) {
    for (String pType : packetTypes) {
      if (pType == packet.getElemName()) {
        log.finest(prependText + packet.toString(secureLogging));
      }
    }
  }
 
  @Override
  public Map<String, Object> getDefaults(Map<String, Object> params) {
    Map<String, Object> defs = super.getDefaults(params);
    defs.put(PACKET_TYPES_KEY, packetTypes);
    defs.put(PREPEND_TEXT_KEY, prependText);
    defs.put(SECURE_LOGGING_KEY, secureLogging);
    return defs;
  }
 
  @Override
  public void setProperties(Map<String, Object> props) {
    super.setProperties(props);
    packetTypes = (String[])props.get(PACKET_TYPES_KEY);
    // Make sure we can compare element names by reference
    // instead of String content
    for (int i = 0; i < packetTypes.length; i++) {
      packetTypes[i] = packetTypes[i].intern();
    }
    prependText = (String)props.get(PREPEND_TEXT_KEY);
    secureLogging = (Boolean)props.get(SECURE_LOGGING_KEY);
  }
 
}

Of course we can do much more useful packet processing in processPacket(...) method. This is just a code example. Please note comparing packet element name with our packet type by reference is intentional and allowed in this context. All Element names are processed with String.intern() function to preserve memory and improve performance of string comparation.

Application: 

Comments

Hi,
There is a lot of debate on using String.intern(), e.g.at http://stackoverflow.com/questions/1091045/is-it-good-practice-to-use-java-lang-string-intern
One of the main issue is that to make it effective, we have to use intern every where in the code. So it is the developer's responsibility to use intern while creating components and plugins while dealing with the Element class. Please advice if I am wrong.

Of course you are right.
But if you use a library (like tigase-xmltools) which hides all the technical details related to objects creation (in Element class) and this library takes care of using intern() method everywhere it is necessary then you can be sure that they are.
So in Tigase intern() is only applied to very specific and limited cases like XML stuff. You can't assume intern() for all possible strings.

For the record: in the tigase.xml.Element class (in the tigase-xmltools-3.3.4.jar) you don't intern the attName parameter of the getAttribute method. This and the use of an IndentityHashMap in that class gave me a couple of strange bugs and sleepless nights.

And, because in the first step the String.equals checks the reference equality of the two comparable strings you can have some performance benefit using intern but comparing strings width equals(). Faster and fail-proof.

First of all, thank you for reporting the problem. That is a nasty bug indeed. I have just fixed it and the version 3.3.6-SNAPSHOT (and SVN trunk) contain the fix now.
I am sorry for problems it caused to you, the problem survived undetected for quite long time.
I use String.intern() for some stuff related to XML handling as it does matter when you have to process 100k XMPP stanzas per second.

I have done some further research on this problem, as I was curious why the problem was undetected for such a long time.
I found that in all cases where I use getAttribute() method, I use either string directly: "attr-name" or a String constant. Such direct strings or constants are intern-ed automatically by JVM (compiler), therefore I have never had a problem with this.
Even though I have changed getAttribute() to perform intern() on method parameter, there is a performance penalty for this. Calling intern is an expensive operation after all.
Therefore I am going to extend Element API to add a method: getAttributeFast(String key) which does not perform intern() on the parameter, hence it would be much faster. It can be used when you pass a string constant, hence it should work correctly.

You do not have to implement getDefaults(...) method and provide default settings for your configuration but doing so gives you a few benefits. The first, from the developer point of view, you don't have to check in the setProperties(...) whether the value is null as it always be either the default or user provided.

I have observed that when configuration is read from init.properties, each key is individually applied using a call to setProperties, whereby the props structure contains only the property being applied. So, you should not just blindly assume all keys are present like in the above example, or you might end up nulling many of your existing properties when only one has changed.

You are correct.
Earlier, Tigase API always made sure that all default properties (some overridden by configuration change) provided in getDefaults() are always passed to the setProperties(...) method. So as long as you took care of writing your getDefaults(...) method you would not have to worry about nulls.
However, this approach was not so good for updating properties during run-time. As we have just started to implement ad-hoc commands for updating configuration at run-time we found that providing all properties, those changes by the ad-hoc command and all others introduces lots of unnecessary complexity to the component. As the component would have to check which option has been changed and which is the same.
For some options it does not matter but for others, like DB access you have to reinitialize the DB connection, sometimes you have to reload/reinitialize some plugins....
So we found out it is much more efficient to provide to component only options modified by configuration change call.
In such case all the component has to do, is to check the option against null value.