This is a guide for Tigase developers. All those people who want to write extensions for Tigase server or participate in work on core server.
The API changes can affect you only if you develop own code to run inside the Tigase server. The changes are not extensive but in some circumstances may require many simple changes in a few files.
All the changes are related to introducing tigase.xmpp.JID and tigase.xmpp.BareJID classes. It is recommended to use them for all operations performed on the user JID instead of the String class which was used before changes.
There are a few advantages of using the new classes. First of all they do all the user JID checking and parsing, they also perform stringprep processing. Therefore if you use data kept by instance of the JID or BareJID you can be sure they are valid and correct.
These are not all advantages however. Working with a profiler and optimising the Tigase code I noticed that a lot of CPU power is used by JID parsing code. JIDs and parts of the JIDs are used in many places of the stanza processing and the parsing is performed over and over again in all these places, wasting CPU cycles, memory and time.
Therefore, great benefits from these new class are in performance if once parsed JIDs are reused in all further stanza processing.
This is where the tigase.server.Packet class comes in handy. Instance of the Packet class encloses XML stanza and pre-parses some, the most commonly used elements of the stanza. Stanza source and destination addresses are among them.
As an effect there are all new methods available in the class:
JID getStanzaFrom(); JID getStanzaTo(); JID getFrom(); JID getTo(); JID getPacketFrom(); JID getPacketTo();
whereas following methods are still available but have been deprecated:
String getElemFrom(); String getElemTo();
Please refer to the JavaDoc documentation for the Packet class and methods to learn all the details of these methods and difference between them.
Another difference is that you can no longer create the Packet instance using a constructor. Instead there are a few factory methods available:
static Packet packetInstance(Element elem);
static Packet packetInstance(Element elem,
JID stanzaFrom, JID stanzaTo);
Again, please refer to the JavaDoc documentation for all the details. The main point of using these methods is that they actually return an instance of one of the following classes instead of the Packet class: Iq, Presence or Message.
There is also a number of utility methods helping with creating a copy of the Packet instance preserving as much pre-parsed data as possible:
Packet copyElementOnly(); Packet errorResult(...); Packet okResult(...); Packet swapFromTo(); Packet swapStanzaFromTo();
Again, I tried to keep the JavaDoc comments as complete as possible, have a look. In case of doubts please contact me and will add missing information to the documentation.
The main point is to reuse JID or BareJID instances in your code as much as possible. You never know, your code may run in highly loaded systems with throughput of 100k XMPP packets per second.
Another change. This one a bit risky as it is very difficult to find all places where this could be used. There are several utility classes and methods which accept source and destination address of a stanza and produce something. There was a great confusion with them, as in some of them the first was the source address and in others the destination address. I have re-factored all the code to keep the parameter order the same in all places. Right now the policy is: source address first.
Therefore in all places where there was a method:
Packet method(String to, String from);
it has been changed to:
Packet method(JID from, JID to);
As far as I know most of these method were used only by myself so I do not expect much trouble for other developers.
Well there are many reasons but the main is that I am the only one developer working on source code at the moment. So the whole approach is to make life easier for me, make the project easier to maintain and development more efficient.
Here is the list:
What features of JDK-1.5 are critical for Tigase development? Why I can't simply reimplement some code to make it compatible with earlier JDK versions?
I think above list is enough to decide to use JDK-1.5. But why JDK-1.6? Well, the is actually only 1 main reason so far:
To make it easier to get into the code below are defined basic terms in Tigase server world and there is a brief explanation how the server is designed and implemented. This document also points you to basic interfaces and implementations which can be used as example code reference.
Logically all server code can be divided into 3 kinds of modules: components, plug-ins and connectors.
There is API defined for each kind of above modules and all you have to do is implementation of specific interface. Then the module can be loaded to the server based on configuration settings. There are also available abstract classes implementing these interfaces to make development easier.
Here is a brief list of all interfaces to look at and for more details you have to refer to the guide for specific kind of module.
tigase.server.ServerComponent - this is the very basic interface for component. All components must implement it.tigase.server.MessageReceiver - this interface extends ServerComponent and is required to implement by components which want to receive data packets like session manager, c2s connection manager and so on...tigase.conf.Configurable - implementing this interface is required to make it configurable. For each object of this type configuration is pushed to it at any time at runtime. This is necessary to make it possible to change configuration at runtime. Implementation should be careful enough to handle this properly.tigase.disco.XMPPService - Objects of which inherit this interface can respond to "ServiceDiscovery" requests.tigase.stats.StatisticsContainer -Objects which inherits this type can return runtime statistics. Any object can collect job statistics and implementing this interface guarantees that statistics will be presented in consisted way to user who wants to see them.Instead of implementing above interfaces directly I would recommend to extend one of existing abstract classes which take care of the most of "dirty and boring" stuff. Here is a list the most useful abstract classes:
tigase.server.AbstractMessageReceiver - implements 4 basic interfaces: ServerComponent, MessageReceiver, Configurable and StatisticsContainer. It also manages internal data queues using own threads which prevents from dead-locks. It offers even-driven data processing which means whenever packet arrives abstract void processPacket(Packet packet); method is called to process it. You have to implement this abstract method in your component. If your component wants to send a packet (in response to data it received for example) it needs to call boolean addOutPacket(Packet packet) method. This is it, I mean basic implementation.tigase.server.ConnectionManager - this is an extension of AbstractMessageReceiver abstract class. As its name says this class takes care of all network connection management stuff. If your component needs to send and receive data directly from the network (like c2s connection, s2s connection or external component) you should use this implementation as a basic class. It takes care of all things related to networking, I/O, reconnecting, listening on socket, connecting and so on. If you extend this class you have to expect data coming from to sources: from the MessageRouter and this is when abstract void processPacket(Packet packet); method is called and from network connection and then abstract Queue processSocketData(XMPPIOService serv); method is called.tigase.xmpp.impl. You can use this code as a sample code base. There are 3 types of plug-ins and they are defined in interfaces located in tigase.xmpp package:
XMPPProcessorIfc - the most important and basic plug-in. This is the most common plug-in type which just processes stanzas in normal mode. It receives packets, processes them on behalf of the user and returns resulting stanzas.XMPPPreprocessorIfc -XMPPPostprocessorIfc -Data received from the network are read from the network sockets as bytes by code in tigase.io package. Bytes then are changed into characters in classes of tigase.net package and as characters they are put to XML parser (tigase.xml) which turns them to XML DOM structures.
All data inside the server are exchanged in XML DOM form as this is the format used by XMPP protocol. For basic XML data processing (parsing characters stream, building DOM, manipulate XML elements and attributes) we use Tigase XML parser and DOM builder.
Each stanza is stored in tigase.xml.Element object. Every Element can contain any number of child Elements and any number of attributes. You can access all these data through the class API.
To simplify some, most common operations Element is wrapped in tigase.server.Packet class which offer another level of API for the most common operations like preparation of response stanza based on the element it contains (swap to/from values, put type=result attribute and so on...).
Tests are very important part of Tigase server development process.
Each release goes through fully automated testing process. All server functions are considered implemented only when they pass testing cycle. Tigase test suite is used for all our automatic tests which allows to define different test scenarios.
There is no tweaking on databases for tests. All databases are installed in standard way and run with default settings. Database is cleared each time before test cycle starts.
There are no modifications to Tigase configuration file as well. All tests are performed on default configuration generated by configuration wizards.
The server is tested in all supported environments:
Functional tests - basic checking if all the functions work at correctly. These tests are performed every time the code is sent to source repository.
| Version | XMLDB | MySQL | PGSQL | Distributed |
|---|---|---|---|---|
| 3.3.2-b889 | 00:00:12 | 00:00:17 | 00:00:17 | none |
| 3.3.2-b880 | 00:00:13 | 00:00:15 | 00:00:15 | None |
| 3.0.2-b700 | 00:00:22 | 00:00:24 | 00:00:25 | 00:00:25 |
| 2.9.5-b606 | 00:00:22 | 00:00:24 | 00:00:24 | 00:00:24 |
| 2.9.3-b548 | 00:00:22 | 00:00:23 | 00:00:25 | 00:00:25 |
| 2.9.1-b528 | 00:00:21 | 00:00:23 | 00:00:24 | 00:00:25 |
| 2.8.6-b434 | 00:00:21 | 00:00:24 | 00:00:24 | 00:00:25 |
| 2.8.5-b422 | 00:00:21 | 00:00:24 | 00:00:24 | 00:00:26 |
| 2.8.3-b409 | 00:00:27 | 00:00:29 | 00:00:29 | 00:00:32 |
| 2.7.2-b378 | 00:00:30 | 00:00:34 | 00:00:33 | 00:00:35 |
| 2.6.4-b300 | 00:00:30 | 00:00:32 | 00:00:35 | 00:00:39 |
| 2.6.4-b295 | 00:00:29 | 00:00:32 | 00:00:45 | 00:00:36 |
| 2.6.0-b287 | 00:00:31 | 00:00:34 | 00:00:47 | 00:00:43 |
| 2.5.0-b279 | 00:00:30 | 00:00:34 | 00:00:45 | 00:00:43 |
| 2.4.0-b263 | 00:00:29 | 00:00:33 | 00:00:45 | 00:00:44 |
| 2.3.4-b226 | None | 00:00:48 | None | None |
Performance tests - checking whether the function performs well enough.
| Version | XMLDB | MySQL | PGSQL | Distributed |
|---|---|---|---|---|
| 3.3.2-b889 | 00:12:17 | 00:13:42 | 00:17:10 | none |
| 3.3.2-b880 | 00:13:39 | 00:14:09 | 00:17:39 | None |
| 3.0.2-b700 | 00:10:26 | 00:11:00 | 00:12:08 | 00:24:05 |
| 2.9.5-b606 | 00:09:54 | 00:11:18 | 00:37:08 | 00:16:20 |
| 2.9.3-b548 | 00:10:00 | 00:11:29 | 00:36:43 | 00:16:47 |
| 2.9.1-b528 | 00:09:46 | 00:11:15 | 00:36:12 | 00:16:36 |
| 2.8.6-b434 | 00:10:02 | 00:11:45 | 00:36:36 | 00:17:36 |
| 2.8.5-b422 | 00:12:37 | 00:14:40 | 00:38:59 | 00:21:40 |
| 2.8.3-b409 | 00:12:32 | 00:14:26 | 00:37:57 | 00:21:26 |
| 2.7.2-b378 | 00:12:28 | 00:14:57 | 00:37:09 | 00:22:20 |
| 2.6.4-b300 | 00:12:46 | 00:14:59 | 00:36:56 | 00:35:00 |
| 2.6.4-b295 | 00:12:23 | 00:14:59 | 00:42:24 | 00:30:18 |
| 2.6.0-b287 | 00:13:50 | 00:16:53 | 00:48:17 | 00:49:06 |
| 2.5.0-b279 | 00:13:29 | 00:16:58 | 00:47:15 | 00:41:52 |
| 2.4.0-b263 | 00:13:20 | 00:16:21 | 00:43:56 | 00:42:08 |
| 2.3.4-b226 | None | 01:23:30 | None | None |
Stability tests - checking whether the function behaves well in long term run. It must handle hundreds of requests a second in several hours server run.
| Version | XMLDB | MySQL | PGSQL | Distributed |
|---|---|---|---|---|
| 2.3.4-b226 | None | 16:06:31 | None | None |
If you want to write a code for Tigase server you might want to use Eclipse. Here is a guide how to start working on source code using this IDE.
All you need to start is:
Click on image to see it in full size.
After installation JDK-1.6.0 in your operating system, run Eclipse and select Window/Preferences.
In section Java/Installed JREs press Add button. In the new opened window enter path to installed JDK-6. In my case it is /opt/jdk1.6.0. It also good to set name to sun-jdk-1.6.0.
As Eclipse doesn't contain built-in support for Subversion repositories you have to add new pluggin.
Detailed instruction for Subclipse installation is on page: subclipse.tigris.org/install.html.
From menu File in Eclipse execute Import. Next, highlight section Team/Team Project Set and press Next.
Enter file name tigase-server.psf in field File and press Finish.
The file is attached to this article.
Because kobit has objections to add Eclipse configuration files to subversion repository you have to do it on your own.
That's it. Start hacking now!
This is a set of documents explaining details what is plugin, how it is designed and how it works inside the Tigase server. The last part of the documentation explains step by step creating the code for a new plugin.
For the Tigase server plugin development it is important to understand how it all works. There are different kind of plugins responsible for processing packets at different stages of the data flow. Please read the introduction below before proceeding to the actual coding part.
In the Tigase server plugins are pieces of code responsible for processing particular XMPP stanza. A separate plugin might be responsible for processing messages, a different one for processing presences, and there might a separate plugins responsible for iq roster, different for iq version and so on.
A plugin provides information about what exact XML element(s) name(s) with xmlns it is interested in. So you can create a plugin which is interested in all packets containing caps child.
There might be no plugin for a particular stanza element and then a default actions is used which is simple forwarding stanza to a destination address. There might be also more than one plugin for a specific XML element and then they all process the same stanza simultaneously in separate threads so there is no guarantee on the order in which the stanza is processed by a different plugins.
Each stanza goes through the Session Manager component which processes packets in a few steps. Have a look at the picture below:
The picture shows that each stanza is processed by the session manager in 4 steps:
Important thing to note is that we have two kinds or two places where packets may be blocked or filtered out. One place is before packet is processed by the plugin and another place is after processing where filtering is applied to all results generated by the processor plugins.
It is also important to note that session manager and processor plugins act as packet consumers. The packet is taken for processing and once processing is finished the packet is destroyed. Therefore to forward a packet to a destination one of the processor must create a copy of the packet, set all properties and attributes and return it as a processing result. Of course processor can generate any number of packets as a result. Result packets can be generated in any of above 4 steps of the processing. Have a look at the picture below:

If the packet P1 is send outside of the server, for example to a user on another server or to some component (MUC, PubSub, transport) then one of the processor must create a copy P2 of the packet and set all attributes and destination addresses correctly. Packet P1 has been consumed by the session manager during processing and a new packet has been generated by one of the plugins.
The same of course happens on the way back from the component to the user:

The packet from the component is processed and one of the plugins must generate a copy of the packet to deliver it to the user. Of course packet forwarding is a default action which is applied when there is no plugin for the particular packet.
It is implemented this way because the input packet P1 can be processed by many plugins at the same time therefore the packet should be in fact immutable and must not change once it got to the session manager for processing.
The most obvious processing workflow is when a user sends request to the server and expects a response from the server:

This design has one surprising consequence though. If you look at the picture below showing communication between 2 users you can see that the packet is copied twice before it is delivered to a final destination:

The packet has to be processed twice by the session manager. The first time it is processed on behalf of the User A as an outgoing packet and the second time it is processed on behalf of the User B as an incoming packet.
This is to make sure the User A has permission to send a packet out and all processing is applied to the packet and also to make sure that User B has permission to receive the packet and all processing is applied. If, for example, the User B is offline there is offline message processor which should put the packet to a database.
Previous guide describes a basic idea behind the XMPP stanza processing in the session manager. As it was already point out the processing takes place in 4 steps. A different kind of plugin is responsible for each step of processing:
If you look inside any of these interfaces you find only a single method. This is it. This is where all the packet processing takes place. All of them take a similar set of parameters and below is a description for all of them:
After a closer look in some of the interfaces you can see that they extend another interface: XMPPImplIfc which provides a basic meta information about the plugin implementation. Please refer to JavaDoc documentation for all details.
For purpose of this guide we are implementing a simple plugin handling all <message/> packets, that is forwarding packets to the destination address. Incoming packets are forwarded to the user connection and outgoing packets are forwarded to the external destination address. This message plugin is actually implemented already and it is available in our SVN repository. The code has some comments inside already but this guide goes deeper into the implementation details.
First of all you have to chose what kind of plugin you want to implement. If this is going to be a packet processor you have to implement XMPPProcessorIfc interface, if this is going to be pre-processor then you have to implement XMPPPreprocessorIfc interface. Of course your implementation can implement more than one interface, even all. It depends on your use case and needs. There is also an abstract helper class which you should use as a base for all you plugins. The class declaration should look like this (assuming you implement just packet processor):
public class Message extends XMPPProcessor implements XMPPProcessorIfc
The first thing to create is the plugin ID. This is a unique string which you put in the configuration file to tell the server to load and use the plugin. In most cases you can use XMLNS if the plugin wants packets with elements with a very specific name space. Of course there is no guarantee there is no other packet for this specific XML element too. As I want to process all messages and I don't want to spend whole day on thinking about a cool ID let's say our ID is: 'message'.
A plugin informs about it's ID using following code:
private static final String ID = "message";
public String id() { return ID; }As I mentioned before such a plugin receives only this kind of packets for processing which it is interested in. My plugin is interested only in packets with <message/> elements and only if they are in "jabber:client" namespace. To indicate all supported elements and namespaces we have to add 2 more methods:
public String[] supElements() {
return new String[] {"message"};
}
public String[] supNamespaces() {
return new String[] {"jabber:client"};
}Now we have our plugin prepared for loading it to the Tigase server. The next step is the actual packet processing method. For the complete code, please refer to the plugin in the SVN. I will only comment here on elements which might be confusing or add a few more lines of code which might be helpful in your case.
public void process(final Packet packet,
final XMPPResourceConnection session,
final NonAuthUserRepository repo,
final Queue<Packet> results,
final Map<String, Object> settings)
throws XMPPException {
// For performance reasons it is better to do the check
// before calling logging method.
if (log.isLoggable(Level.FINEST)) {
log.finest("Processing packet: " + packet.toString());
}
// You may want to skip processing completely if the user is offline.
if (session == null) {
return;
} // end of if (session == null)
// There is also a way to execute some actions on the first
// run of the plugin for the user session:
if (session.getSessionData(ID) == null) {
session.putSessionData(ID, ID);
// Put your code here:
.....
// You may not finish execution of the plugin or continue
return;
}
// If the user session is not authenticated yet every call
// to session.getUserId() throws an exception.
try {
// Remember to cut the resource part off before comparing JIDs
String id = JIDUtils.getNodeID(packet.getElemTo());
// Checking if this is a packet TO the owner of the session
if (session.getUserId().equals(id)) {
// Yes this is message to 'this' client
Element elem = packet.getElement().clone();
Packet result = new Packet(elem);
// This is where and how we set the address of the component
// which should rceive the result packet for the final delivery
// to the end-user. In most cases this is a c2s or Bosh component
// which keep the user connection.
result.setTo(session.getConnectionId(packet.getElemTo()));
// In most cases this might be skept, however if there is a
// problem during packet delivery an error might be sent back
result.setFrom(packet.getTo());
// Don't forget to add the packet to the results queue or it
// will be lost.
results.offer(result);
} // end of else
// Remember to cut the resource part off before comparing JIDs
id = JIDUtils.getNodeID(packet.getElemFrom());
// Checking if this is maybe packet FROM the client
if (session.getUserId().equals(id)) {
// This is a packet FROM this client, the simplest action is
// to forward it to is't destination:
// Simple clone the XML element and....
Element result = packet.getElement().clone();
// ... putting it to results queue is enough
results.offer(new Packet(result));
return;
}
// Can we really reach this place here?
// Yes, some packets don't even have from or to address.
// The best example is IQ packet which is usually a request to
// the server for some data. Such packets may not have any addresses
// And they usually require more complex processing
// This is how you check whether this is a packet FROM the user
// who is owner of the session:
id = packet.getFrom();
// This test is in most cases equal to checking getElemFrom()
if (session.getConnectionId().equals(id)) {
// Do some packet specific processing here, but we are dealing
// with messages here which normally need just forwarding
Element result = packet.getElement().clone();
// If we are here it means FROM address was missing from the
// packet, it is a place to set it here:
result.setAttribute("from", session.getJID());
// ... putting it to results queue is enough
results.offer(new Packet(result));
}
} catch (NotAuthorizedException e) {
log.warning("NotAuthorizedException for packet: " +
packet.getStringData());
results.offer(Authorization.NOT_AUTHORIZED.getResponseMessage(packet,
"You must authorize session first.", true));
} // end of try-catch
}
Plugin configuration is not very straightforward at the moment but we are going to change it soon.
At the moment the best and the simplest way to tell the Tigase server to load or not to load the plugin is via init.properties file. Property --sm-plugins takes a comma separated list of plugin IDs to active at the runtime. Please refer to the documentation for complete description.
Obviously you have to know the list of standard plugin IDs to add your to the set. There are 2 ways to find out the list. One is the log file: logs/tigase-console.log. If you look inside you can find following output:
Loading plugin: jabber:iq:register ... Loading plugin: jabber:iq:auth ... Loading plugin: urn:ietf:params:xml:ns:xmpp-sasl ... Loading plugin: urn:ietf:params:xml:ns:xmpp-bind ... Loading plugin: urn:ietf:params:xml:ns:xmpp-session ... Loading plugin: roster-presence ... Loading plugin: jabber:iq:privacy ... Loading plugin: jabber:iq:version ... Loading plugin: http://jabber.org/protocol/stats ... Loading plugin: starttls ... Loading plugin: vcard-temp ... Loading plugin: http://jabber.org/protocol/commands ... Loading plugin: jabber:iq:private ... Loading plugin: urn:xmpp:ping ...
and this is a list of plugins which are loaded in your installation.
Another way is to look inside the session manager source code which has the default list hardcoded:
private static final String[] PLUGINS_FULL_PROP_VAL =
{"jabber:iq:register", "jabber:iq:auth", "urn:ietf:params:xml:ns:xmpp-sasl",
"urn:ietf:params:xml:ns:xmpp-bind", "urn:ietf:params:xml:ns:xmpp-session",
"roster-presence", "jabber:iq:privacy", "jabber:iq:version",
"http://jabber.org/protocol/stats", "starttls", "msgoffline",
"vcard-temp", "http://jabber.org/protocol/commands", "jabber:iq:private",
"urn:xmpp:ping", "basic-filter", "domain-filter"};
In any way you have to put the list and your plugin IDs as a value to the plugin list property. Let's say our plugin ID is 'message' as in our all examples:
--sm-plugins=jabber:iq:register,jabber:iq:auth,......,message
Assuming your plugin class is in the classpath it will be loaded and used at the runtime.
There is another part of the plugin configuration though. If you looked at the writing plugin code guide you can remember Map settings processing parameter. This is a map of properties you can set in the configuration file and these setting will be passed to the plugin at the processing time.
Again init.properties is the place to put the stuff. This kind of properties start with a string: sess-man/plugins-conf/, then you add your plugin ID and at the end your setting key and value:
sess-man/plugins-conf/message/key1=val1 sess-man/plugins-conf/message/key2=val2 sess-man/plugins-conf/message/key3=val3
The purpose of this guide is to introduce to the vhost management in the Tigase server. Please refer to the JavaDoc documentation for all details. All interfaces are well documented and you can use existing implementation as an example code base and reference point. The VHost management files are located in the SVN repository and you can browse them using the project tracker.
Virtual hosts management in the Tigase server can be adjusted in many ways through the flexible API. The core elements of the virtual domains management is interface VHostManagerIfc and its implementation VHostManager class. They are responsible for providing the virtual hosts information to the rest of the Tigase server components. In particular to the MessageRouter class which controls XMPP packets flow inside the server.
The class you most likely want to re-implement is VHostJDBCRepository used as a default virtual hosts storage and implementing interface VHostRepository. You might need to have your own implementation in order to store and access virtual hosts in other than Tigase own data storage. This is especially important if you are going to modify the virtual domains list through other than Tigase server system.
The very basic virtual hosts storage is provided by VhostConfigRepository class. This is read only storage and provides the server a bootstrap vhosts data at the first startup time when the database with virtual hosts is empty or is not accessible. Therefore it is advised that all VHostRepository implementation extend this class. The example code is provided in the VHostJDBCRepository file.
All components which may need virtual hosts information or want to interact with virtual hosts management subsystem should implement VHostListener interface. In some cases implementing this interface is also necessary to receive packets for processing.
Virtual host information is carried out in 2 forms inside the Tigase server:
String value with the domain nameHere is a complete list of all interfaces and classes with a brief description for each of them:
Tigase Test Suite is an engine which allows you to run tests. Essentially it just executes TestCase implementations. The tests may depend on other tests which means they are executed in specific order. For example authentication test is executed after the stream open test which in turn is executed after network socket connection test.
The tests may have parameters. Each TestCase implementation may have it's own set of specific parameters. There is a set of common parameters which may be applied to any TestCase. As an example of the common parameter you can take -loop = 10 which specified that the TestCase must be execited 10 times. The test specific parameter might be -user-name = tester which may set the user name for authentication test.
The engine is very generic and allows you to write any kind of tests but for the Tigase projects the current TestCase implementations mimic an XMPP client and are designed to test XMPP servers.
The suite contains also kind of scripting language which allows you to combine test cases into a test scenarios. The test scenario may contain full set of functional tests for example, another test scenario may contain performance tests and so on.
The test suite contains also scripting language which allows you to combine test cases into a test scenarios. On the lowest level, however the language is designed to allow you to describe the test by setting test parameters, test comments, identification and so on.
Let's look at the example test description.
Short name@test-id-1;test-id-2: Short description for the test case
{
-loop = 10
-user-name = Frank
# This is a comment which is ignored
}
>> Long, detailed description of the test case <<
Meaning of all elements:
Between an open curly bracket { and close one } you can put all the test case parameters you wish. The format for it is:
-parameter-name = value
Parameter names always start with '-'. Note, some parameters don't need any value. They can exist on their own without any value assigned:
-debug-on-error
It works like you set "yes" or "true" for this parameter but you don't set anything.
The scripting language includes also support for variables which can be assigned any value and used multiple times later on. You assign a value to the variable the same way as you assign it to the parameter:
$(variable-name) = value
The variable name must be always enclosed with brackets () and start with '$'.
The value may be enclosed within double quotes "" or double quotes may be omitted. If this is a simple string like a number or character string consisting only of digits, letters, underscore '_' and hyphen '-' then you can omit double quotes otherwise you must enclose the value.
The test case descriptions can be nested inside other test case descriptions. Nested test case descriptions inherit parameters and variables from outer test case description.
There is long list of parameters which can be applied to any test case. Here is the description of all possible parameters which can be used to build test scenarios.
There are test report parameters which must be set in the main script file in order to generate HTML report from the test. These parameters have no effect is they are set inside the test case description.
These parameters can be set on per-test case basis but usually they are set in the main script file to apply them to all test cases.
Test parameters which are normally set on per-test case basis and apply only to the test they are set for and all inherited tests. Some of the parameters though are applied only to inherited test cases. Please look in the description below to find more details.
You can write tests in form of simple text file which is loaded during test suite runtime.
You simply specify what should be send to the server and what response should be expected from the server. No need to write Java code and recompile whole test suite for new tests. It means new test cases can be now written easily and quickly which hopefully means more detailed tests for the server.
How it works:
Let's take XEP-0049 Private XML Storage. Looking into the spec we can see the first example:
Example 1. Client Stores Private Data
CLIENT:
<iq type="set" id="1001">
<query xmlns="jabber:iq:private">
<exodus xmlns="exodus:prefs">
<defaultnick>Hamlet</defaultnick>
</exodus>
</query>
</iq>
SERVER:
<iq type="result" id="1001"/>
This is enough for the first simple test. I have to create text file JabberIqProvate.test looking like this:
send: {
<iq type="set" id="1001">
<query xmlns="jabber:iq:private">
<exodus xmlns="exodus:prefs">
<defaultnick>Hamlet</defaultnick>
</exodus>
</query>
</iq>
}
expect: {
<iq type="result" id="1001"/>
}
And now I can execute the test:
testsuite $ ./scripts/all-tests-runner.sh --single JabberIqPrivate.test Tigase server home directory: ../server Version: 2.8.5-b422 Database: xmldb Server IP: 127.0.0.1 Extra parameters: JabberIqPrivate.test Starting Tigase: Tigase running pid=6751
Running: 2.8.5-b422-xmldb test, IP 127.0.0.1...
Script name: scripts/single-xmpp-test.xmpt
Common test: Common test ... failure!
FAILURE, (Received result doesn't match expected result.,
Expected one of: [<iq id="1001" type="result"/>],
received:
[<iq id="1001" type="error">
<query xmlns="jabber:iq:private">
<exodus xmlns="exodus:prefs">
<defaultnick>Hamlet</defaultnick>
</exodus>
</query>
<error type="cancel">
<feature-not-implemented xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"/>
<text xml:lang="en" xmlns="urn:ietf:params:xml:ns:xmpp-stanzas">
Feature not supported yet.</text>
</error>
</iq>]),
Total: 100ms
Test time: 00:00:02
Shutting down Tigase: 6751
If I just started working on this XEP and there is no code on the server side the result is perfectly expected although maybe this is not what we want. After a while of working on the server code I can execute the test once again:
testsuite $ ./scripts/all-tests-runner.sh --single JabberIqPrivate.test Tigase server home directory: ../server Version: 2.8.5-b422 Database: xmldb Server IP: 127.0.0.1 Extra parameters: JabberIqPrivate.test Starting Tigase: Tigase running pid=6984
Running: 2.8.5-b422-xmldb test, IP 127.0.0.1... Script name: scripts/single-xmpp-test.xmpt Common test: Common test ... success, Total: 40ms Test time: 00:00:01 Shutting down Tigase: 6984
This is it. The result we want. In simple and efficient way. We can repeat it as many times we want which is especially important in longer term. Every time we change the server code we can re-run tests to make sure we get correct responses from the server.
You can have a look in current, with more complete test cases, file for JabberIqPrivate.
Now my server tests are no longer outdated. Of course not all cases are so simple. Some XEPs require calculations to be done before stanza is sent or to compare received results. A good example for this case is user authentication like SASL and even NON-SASL. But still, there are many cases which can be covered by simple tests: roster management, privacy lists management, vCard, private data storage and so on.....
A component in the Tigase is an entity with own JID address. It can receive packets, can process them and can also generate packets.
An example of the best known components is MUC or PubSub. In the Tigase server, however, almost everything is actually a component: Session Manager, s2s connections manager, Message Router, etc.... Components are loaded based on the server configuration, new components can be loaded and activated at the server run-time. You can easily replace a component implementation and the only change to make is a class name in the configuration entry.
Creating components for the Tigase server is an essential part of the server development hence there is a lot of useful API and ready to use code available. This guide should help you to get familiar with the API and how to quickly and efficiently create own component implementations.
Creating a Tigase component is actually very simple and with broad API available you can create a powerful component with just a few lines of code. You can find detailed API description elsewhere. This series presents hands on lessons with code examples, teaching how to get desired results in the simplest possible code using existing Tigase API.
Even though all Tigase components are just implementations of ServerComponent interface I will keep such a low level information to necessary minimum. Creating a new component based on just interfaces, while very possible, is not very effective. This guide intends to teach you how to make use of all what is already there, ready to use with a minimal coding effort.
This is just the first lesson of the series where I cover basics of the component implementation.
Let's get started and create the Tigase component:
import tigase.server.AbstractMessageReceiver; import tigase.server.Packet; public class TestComponent extends AbstractMessageReceiver { public void processPacket(Packet packet) { log.finest("My packet: " + packet.toString()); } }
The only element mandatory when you extend AbstractMessageReceiver is the implementation of void processPacket(Packet packet) method. This is actually logical as the main task for your component is processing packets. Class name for our new component is TestComponent and we have also initialised a separated logger for this class. This is actually very useful as it allows us to easily find log entries created by our class.
With these a few lines of code you have a fully functional Tigase component which can be loaded to the Tigase server, can receive and process packets, shows as an element on service discovery list (for administrators only), responds to administrator ad-hoc commands, supports scripting, generates statistics, can be deployed as an external component and a few other things.
Before we go any further with the implementation let's set the component in the Tigase server so it is loaded next time the server starts. Assuming our init.properties file looks like this one:
config-type = --gen-config-def --debug = server --user-db = derby --admins = admin@devel.tigase.org --user-db-uri = jdbc:derby:/Tigase/tigasedb --virt-hosts = devel.tigase.org --comp-name-1 = muc --comp-class-1 = tigase.muc.MUCComponent --comp-name-2 = pubsub --comp-class-2 = tigase.pubsub.PubSubComponent
We can see that it already is configured to load two other components: MUC and PubSub. Let's add third - our new component to the configuration file by appending two following lines in the properties file:
--comp-name-3 = test --comp-class-3 = TestComponent
Now we have to remove the etc/tigase.xml file and restart the server.
There are a few ways to check whether our component has been loaded to the server. Probably the easiest is to connect to the server from administrator account and look at the service discovery list.
If everything goes well you should see an entry on the list similar to highlighted on the screenshot. The component description is "Undefined description" which is a default description and we can change it later on, the component default JID is: test@devel.tigase.org, where devel.tigase.org is the server domain and test is the component name.
Another way to find out if the component has been loaded is by looking at log files. Actually getting yourself familiar with Tigase log files will be very useful thing if you plan on developing Tigase components. So let's look at the log file logs/tigase.log.0, if the component has been loaded you should find following lines in the log:
MessageRouter.setProperties() FINER: Loading and registering message receiver: test MessageRouter.addRouter() INFO: Adding receiver: TestComponent MessageRouter.addComponent() INFO: Adding component: TestComponent MessageRouter.addComponent() FINER: Adding: test component to basic-conf registrator. Configurator.componentAdded() CONFIG: component: test
If your component did not load you should first check configuration files. Maybe you forgot to remove the tigase.xml file before restarting the server or alternatively the Tigase could not find your class at startup time. Make sure your class is in CLASSPATH or copy a JAR file with your class to Tigase libs/ directory.
Assuming everything went well and your component is loaded by the Tigase sever and it shows on the service discovery list as on the screenshot above you can double click on it to get a window with a list of ad-hoc commands - administrator scripts. A window on the screenshot shows only two basic commands for adding and removing script which is a good start.
Moreover, you can browse the server statistics in the service discovery window to find your new test component on the list. If you click on the component it shows you a window with component statistics, very basic packets counters.
As we can see with just a few lines of code our new component is quite mighty and can do a lot of things without much effort from the developer side.
Now, the time has come to the most important question. Can our new component do something useful, that is can it receive and process XMPP packets?
Let's try it out. Using you favourite client send a message to JID: test@devel.tigase.org (assuming your server is configured for devel.tigase.org domain). You can either use kind of XML console in your client or just send a plain message to the component JID. According to our code in processPacket(...) method it should log our message. For this test I have sent a message with subject: "test message" and body: "this is a test". The log file should contain following entry:
TestComponent.processPacket() FINEST: My packet: to=null, from=null, data=<message from="admin@devel.tigase.org/devel" to="test@devel.tigase.org" id="abcaa" xmlns="jabber:client"> <subject>test message</subject> <body>this is a test</body> </message>, XMLNS=jabber:client, priority=NORMAL
If this is a case we can be sure that everything works as expected and all we now have to do is to fill the processPacket(...) method with some useful code.
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 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:
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:
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; } public void setProperties(Map<String, Object> props) { super.setProperties(props); }
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. It also will always 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.
Also all configuration parameters for which defaults are provided are saved to tigase.xml configuration file. You can review them, change and actually see whether your component behaves as you expect.
Regardless you implemented the getDefaults(...) method or not you can always manually add parameters to the tigase.xml file or provide them via 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 tigase.server.AbstractMessageReceiver; import tigase.server.Packet; public class TestComponent extends AbstractMessageReceiver { private boolean secureLogging = false; public void processPacket(Packet packet) { if (pType == packet.getElemName()) { log.finest(prependText + packet.toString(secureLogging)); } } } 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; } public void setProperties(Map<String, Object> props) { super.setProperties(props); // 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(); } } }
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.
Multi core and multi CPU machines are nowadays very common. Especially for the application like the XMPP server you most likely deploy your service on a server with a few cores or even a few CPUs. Your new component however processes all packets in a single thread.
This is especially important if the packet processing is CPU expensive like, for example, SPAM checking. In such a case you could experience single Core/CPU usage at 100% while other Cores/CPUs are idling. Ideally, you want your component to use all available CPUs.
The Tigase API offers a very simple way to execute component's processPacket(Packet packet) method in multiple threads. The method int processingThreads() returns number of threads assigned to the component. By default it returns just '1' as not all component implementations are prepared to process packets concurrently. By overwriting the method you can return any value you think is appropriate for the implementation.
If the packet processing is CPU bound only, you normally want to have as many threads as there are CPUs available:
public int processingThreads() { }
If the processing is I/O bound (network or database) you probably want to have much more threads to process requests. It is hard to guess ideal number of threads, instead you should run a few tests to see what exact number is best for the component implementation.
Now you have many threads for processing your packets. There is one slight problem with this, however. In many cases packets order is essential. If our processPacket(...) method is executed concurrently by a few threads it is quite possible that a message sent to user can takeover the message sent earlier. Especially if the first message was large and the second was small. We can prevent this by adjusting method responsible for packets distribution among threads.
The algorithm for packets distribution among threads is very simple:
int thread_idx = hashCodeForPacket(packet) % threads_total;
So the key here is hashCodeForPacket(...) method. By overwriting it we can make sure that all packets addressed to the same user will always be processed by the same thread:
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.getElemFrom() != null) { return packet.getElemFrom().hashCode(); } // If this really happens on your system you should look // carefully at packets arriving to your component and // find a better way to calculate hashCode return 1; }
Above two methods give a control over the number of threads assigned to the packets processing in your component and to the packets distribution among threads. This is not all the Tigase API has to offer in terms of multi-threading.
Sometimes you want to perform some periodic actions. You can of course create Timer instance and load it with TimerTasks but as there might be a need for this on every level of the Class hierarchy you could end-up with multiple Timer (threads in fact) objects doing similar job and using resources. There are a few methods which allow you to reuse common Timer object to perform all sorts of actions.
First, you have three methods allowing your to perform some periodic actions:
public synchronized void everySecond(); public synchronized void everyMinute(); public synchronized void everyHour();
An example implementation for periodic notifications sent to some address could look like this one:
public synchronized void everyMinute() { super.everyMinute(); if ((++delayCounter) >= notificationFrequency) { addOutPacket(Packet.getMessage(abuseAddress, getComponentId(), StanzaType.chat, "Detected spam messages: " + spamCounter, "Spam counter", null, newPacketId("spam-"))); delayCounter = 0; spamCounter = 0; } }
This method sends every 'notificationFrequency' minutes a message to 'abuseAddress' reporting how many spam messages have been detected during last period. Please note, you have to call super.everyMinute() to make sure other actions are executed as well and you have to also remember to keep processing in this method to minimum, especially if you overwrite everySecond() method.
There are also two methods which allow you to schedule tasks executed at certain time, they are very similar to the java.util.Timer API with the only difference is that Timer is reused among all levels of Class hierarchy. There is a separate Timer for each Class instance though, to avoid interferences between separate components:
There is one more method which can be overwritten which is not directly related to multi-threading but might be very helpful for executing some actions at a very specific point of time. This is the point of time when the server has just been initialised, that is all components have been created and received their configuration for the first time. When this happens the Tigase calls void initializationCompleted() method for each server component. You can overwrite this method to execute some actions at the time when you are sure the the Tigase server has started and is fully functional.
And here is a code of an example component which uses all the API discussed in this article:
import tigase.server.AbstractMessageReceiver; import tigase.server.Packet; import tigase.util.JIDUtils; import tigase.xmpp.StanzaType; public class TestComponent extends AbstractMessageReceiver { private int notificationFrequency = 10; private int delayCounter = 0; private boolean secureLogging = false; private long spamCounter = 0; public void processPacket(Packet packet) { // Is this packet a message? if ("message" == packet.getElemName()) { // Is sender on the whitelist? // The sender is not on whitelist so let's check the content if (body != null && !body.isEmpty()) { body = body.toLowerCase(); if (body.contains(word)) { log.finest(prependText + packet.toString(secureLogging)); ++spamCounter; return; } } } } } // Not a SPAM, return it for further processing Packet result = packet.swapFromTo(); addOutPacket(result); } public int processingThreads() { } 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.getElemFrom() != null) { return packet.getElemFrom().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; } public Map<String, Object> getDefaults(Map<String, Object> params) { Map<String, Object> defs = super.getDefaults(params); defs.put(BAD_WORDS_KEY, badWords); defs.put(WHITELIST_KEY, whiteList); defs.put(PREPEND_TEXT_KEY, prependText); defs.put(SECURE_LOGGING_KEY, secureLogging); defs.put(ABUSE_ADDRESS_KEY, abuseAddress); defs.put(NOTIFICATION_FREQ_KEY, notificationFrequency); return defs; } public void setProperties(Map<String, Object> props) { super.setProperties(props); } public synchronized void everyMinute() { super.everyMinute(); if ((++delayCounter) >= notificationFrequency) { addOutPacket(Packet.getMessage(abuseAddress, getComponentId(), StanzaType.chat, "Detected spam messages: " + spamCounter, "Spam counter", null, newPacketId("spam-"))); delayCounter = 0; spamCounter = 0; } } }
You component still shows in the service discovery list as an element with "Undefined description". It doesn't also provide any interesting features or sub-nodes.
In this article I will show how, in a simple way, change the basic component information presented on the service discovery list, 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.
Component description and category type can be changed by overwriting two following methods:
return "Spam filtering"; } return "spam"; }
Please note, there is no such category type like 'spam' defined in the Service Discovery Identities registry. It has been used here as a demonstration only. Please refer to the document mentioned above for a list of categories and types and pick the one most suitable to you.
After you added 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.
This was easy but just 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 setProperties(...) method call and the component service discovery information is created and prepared for later use. Sometimes, however it is very useful to be able to change the service discovery at 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, the 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.
The complete method code is presented below and screenshot on the left 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 use case is good enough to demonstrate the API available in the Tigase server 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 recognised 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 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 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'. The method accepts 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, PubSub which 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:
import tigase.server.AbstractMessageReceiver; import tigase.server.Packet; import tigase.util.JIDUtils; import tigase.xmpp.StanzaType; public class TestComponent extends AbstractMessageReceiver { private int notificationFrequency = 10; private int delayCounter = 0; private boolean secureLogging = false; private long spamCounter = 0; private long totalSpamCounter = 0; private long messagesCounter = 0; public void processPacket(Packet packet) { // Is this packet a message? if ("message" == packet.getElemName()) { updateServiceDiscoveryItem(getName(), "messages", "Messages processed: [" + (++messagesCounter) + "]", true); // Is sender on the whitelist? // The sender is not on whitelist so let's check the content if (body != null && !body.isEmpty()) { body = body.toLowerCase(); if (body.contains(word)) { log.finest(prependText + packet.toString(secureLogging)); ++spamCounter; updateServiceDiscoveryItem(getName(), "spam", "Spam caught: [" + (++totalSpamCounter) + "]", true); return; } } } } } // Not a SPAM, return it for further processing Packet result = packet.swapElemFromTo(); addOutPacket(result); } public int processingThreads() { } 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.getElemFrom() != null) { return packet.getElemFrom().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; } public Map<String, Object> getDefaults(Map<String, Object> params) { Map<String, Object> defs = super.getDefaults(params); defs.put(BAD_WORDS_KEY, badWords); defs.put(WHITELIST_KEY, whiteList); defs.put(PREPEND_TEXT_KEY, prependText); defs.put(SECURE_LOGGING_KEY, secureLogging); defs.put(ABUSE_ADDRESS_KEY, abuseAddress); defs.put(NOTIFICATION_FREQ_KEY, notificationFrequency); return defs; } public void setProperties(Map<String, Object> props) { super.setProperties(props); updateServiceDiscoveryItem(getName(), null, getDiscoDescription(), "automation", "spam-filtering", true, "tigase:x:spam-filter", "tigase:x:spam-reporting"); } public synchronized void everyMinute() { super.everyMinute(); if ((++delayCounter) >= notificationFrequency) { addOutPacket(Packet.getMessage(abuseAddress, getComponentId(), StanzaType.chat, "Detected spam messages: " + spamCounter, "Spam counter", null, newPacketId("spam-"))); delayCounter = 0; spamCounter = 0; } } return "Spam filtering"; } return "spam"; } }
In most cases you want to gather some run-time statistics from your component to see how it works, detect possible performance issues or congestion problems. All the server statistics are exposed and are accessible via XMPP with ad-hoc commands, HTTP, JMX and some selected statistics are also available via SNMP. As a component developer you don't have to do anything to expose your statistic via any of above protocols, you just have to provide your statistics and the admin will be able to access them any way he wants.
This lesson will teach you how to add your own statistics and how to make sure that the statistics generation doesn't affect application performance.
Your component from the very beginning generates some statistics by classes it inherits. Let's add a few statistics to our spam filtering component:
public void getStatistics(StatisticsList list) { super.getStatistics(list); // Some very expensive statistics generation code... } }
I think the code should be pretty much self-explanatory.
You have to call 'super.getStatistics(...)' to update stats of the parent class. StatisticsList is a collection which keeps all the statistics in a way which is easy to update them and search and retrieve. You actually don't need to know all the implementation details but if you are interested please refer to the source code and JavaDoc documentation.
The first parameter of the 'add(...)' method is the component name. All the statistics are grouped by the component names to make it easier to look at particular component data. Next is a description of the element. The third parameter is the element value which can be any number or string.
The last parameter is probably the most interesting. The idea has been borrowed from the logging framework. Each statistic item has importance level. Levels are exactly the same as for logging methods with 'SEVERE' the most critical and 'FINEST' the least important. This parameter has been added to improve performance and statistics retrieval. When the 'StatisticsList' object is created it get's assigned a level requested by the user. If 'add(...)' method is called with lower priority level then the element is not even added to the list. This saves network bandwidth, improves statistics retrieving speed and is also more clear to present to the end-user.
One thing which may be a bit confusing at first is that, if there is a numerical element added to statistics with '0' value then the Level is always forced to 'FINEST'. The assumption is that the administrator is normally not interested zero-value statistics, therefore unless he intentionally request the lowest level statistics he won't see elements with zeros.
The 'if' statement requires some explanation too. Normally adding a new statistics element is not a very expensive operation so passing it with 'add(...)' method and appropriate level is enough. Sometimes, however preparing statistics data may be quite expensive, like reading/counting some records from database. Statistics can be collected quite frequently therefore it doesn't make sense to collect the statistics at all if there not going to be used as the current level is higher then the item we pass anyway. In such a case it is recommended to test whether the element level will be accepted by the collection and if not skip the whole processing altogether.
As you can see, the API for generating and presenting component statistics is very simple and straightforward. Just one method to overwrite and a simple way to pass your own counters. Below is the whole code of the example component:
import tigase.server.AbstractMessageReceiver; import tigase.server.Packet; import tigase.stats.StatisticsList; import tigase.util.JIDUtils; import tigase.xmpp.StanzaType; public class TestComponent extends AbstractMessageReceiver { private int notificationFrequency = 10; private int delayCounter = 0; private boolean secureLogging = false; private long spamCounter = 0; private long totalSpamCounter = 0; private long messagesCounter = 0; public void processPacket(Packet packet) { // Is this packet a message? if ("message" == packet.getElemName()) { updateServiceDiscoveryItem(getName(), "messages", "Messages processed: [" + (++messagesCounter) + "]", true); // Is sender on the whitelist? // The sender is not on whitelist so let's check the content if (body != null && !body.isEmpty()) { body = body.toLowerCase(); if (body.contains(word)) { log.finest(prependText + packet.toString(secureLogging)); ++spamCounter; updateServiceDiscoveryItem(getName(), "spam", "Spam caught: [" + (++totalSpamCounter) + "]", true); return; } } } } } // Not a SPAM, return it for further processing Packet result = packet.swapElemFromTo(); addOutPacket(result); } public int processingThreads() { } 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.getElemFrom() != null) { return packet.getElemFrom().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; } public Map<String, Object> getDefaults(Map<String, Object> params) { Map<String, Object> defs = super.getDefaults(params); defs.put(BAD_WORDS_KEY, badWords); defs.put(WHITELIST_KEY, whiteList); defs.put(PREPEND_TEXT_KEY, prependText); defs.put(SECURE_LOGGING_KEY, secureLogging); defs.put(ABUSE_ADDRESS_KEY, abuseAddress); defs.put(NOTIFICATION_FREQ_KEY, notificationFrequency); return defs; } public void setProperties(Map<String, Object> props) { super.setProperties(props); updateServiceDiscoveryItem(getName(), null, getDiscoDescription(), "automation", "spam-filtering", true, "tigase:x:spam-filter", "tigase:x:spam-reporting"); } public synchronized void everyMinute() { super.everyMinute(); if ((++delayCounter) >= notificationFrequency) { addOutPacket(Packet.getMessage(abuseAddress, getComponentId(), StanzaType.chat, "Detected spam messages: " + spamCounter, "Spam counter", null, newPacketId("spam-"))); delayCounter = 0; spamCounter = 0; } } return "Spam filtering"; } return "spam"; } public void getStatistics(StatisticsList list) { super.getStatistics(list); // Some very expensive statistics generation code... } } }
Scripting support is a basic API built-in to the Tigase server and automatically available to any component at no extra cost. This framework, however, can only access existing component variables which are inherited by your code from parent classes. It can not access any data or any structures you added in your component. A little effort is needed to expose some of your data to the scripting API.
This guide shows how to extend existing scripting API with your component specific data structures.
Integrating your component implementation with the scripting API is as simple as the code below:
public void initBindings(Bindings binds) { super.initBindings(binds); binds.put(BAD_WORDS_VAR, badWords); binds.put(WHITE_LIST_VAR, whiteList); }
This way you expose two the component variables: 'badWords' and 'whiteList' to scripts under names the same names - two defined constants. You could use different names of course but it is always a good idea to keep things simple, hence we use the same variable names in the component and in the script.
This is it, actually, all done. Almost... In our old implementation these two variables are Java arrays of 'String's, therefore we can only change their elements but we can not add or remove elements from these structures inside the script. This is not very practical and it puts some serious limits on the script's code. To overcome this problem I have changed the test component code to keep bad words and whitelist in 'java.util.Set' collection. This gives us enough flexibility to manipulate data.
As our component is now ready to cooperate with the scripting API, I will demonstrate now how to add remove or change elements of these collections using a script and ad-hoc commands.
First, browse the server service discovery and double click on the test component. If you use Psi client this should bring to you a new window with ad-hoc commands list. Other clients may present available ad-hoc commands differently.
The screenshot on the right hand side show how this may look like. You have to provide some description for the script and an ID string. We use Groovy in this guide but you can as well use any different scripting language.
Please refer to the Tigase scripting documentation for all the details how to add support for more languages. From the Tigase API point of view it all looks the same. You have to select a proper language from the pull-down list on windows shown on the right. If your preferred language is not on the list, it means it is not installed properly and Tigase couldn't detect it.
The script to pull a list of current bad words can be as simple as the following Groovy code:
As you see from the code, you have to reference your component variables to a variables in your script to make sure a correct type is used. The rest is very simple and is a pure scripting language stuff.
Load the script on to the server and execute it. You should receive a new window with a list of all bad words currently used by the spam filter.
Below is another simple script which allows updating (adding/removing) bad words from the list.
import tigase.server.Command import tigase.server.Packet def WORDS_LIST_KEY = "words-list" def OPERATION_KEY = "operation" def REMOVE = "Remove" def ADD = "Add" // No data to process, let's ask user to provide // a list of words Command.addFieldValue(res, WORDS_LIST_KEY, "", "Bad words list") Command.addFieldValue(res, OPERATION_KEY, ADD, "Operation", return res } return "Words have been added." } return "Words have been removed." } return "Unknown operation: " + operation
These two scripts are just the beginning. The possibilities are endless and with the simple a few lines of code in your test component you can then extend your application at runtime with scripts doing various things, you can reload scripts, add and remove them extending and modifying functionality as you need. No need to restart the server, no need to recompile the code and you can use whatever scripting language you like.
Of course, scripts for whitelist modifications would look exactly the same and it doesn't make sense to attach them here.
Here is a complete code of the test component with the new method described at the beginning and data structures changed from array of 'String's to Java 'Set':
import javax.script.Bindings; import tigase.server.AbstractMessageReceiver; import tigase.server.Packet; import tigase.stats.StatisticsList; import tigase.util.JIDUtils; import tigase.xmpp.StanzaType; public class TestComponent extends AbstractMessageReceiver { /** * This might be changed in one threads while it is iterated in * processPacket(...) in another thread. We expect that changes are very rare * and small, most of operations are just iterations. */ private Set<String> badWords = new CopyOnWriteArraySet<String>(); /** * This might be changed in one threads while it is iterated in * processPacket(...) in another thread. We expect that changes are very rare * and small, most of operations are just contains(...). */ private Set<String> whiteList = new ConcurrentSkipListSet<String>(); private int notificationFrequency = 10; private int delayCounter = 0; private boolean secureLogging = false; private long spamCounter = 0; private long totalSpamCounter = 0; private long messagesCounter = 0; public void processPacket(Packet packet) { // Is this packet a message? if ("message" == packet.getElemName()) { updateServiceDiscoveryItem(getName(), "messages", "Messages processed: [" + (++messagesCounter) + "]", true); // Is sender on the whitelist? if (!whiteList.contains(from)) { // The sender is not on whitelist so let's check the content if (body != null && !body.isEmpty()) { body = body.toLowerCase(); if (body.contains(word)) { log.finest(prependText + packet.toString(secureLogging)); ++spamCounter; updateServiceDiscoveryItem(getName(), "spam", "Spam caught: [" + (++totalSpamCounter) + "]", true); return; } } } } } // Not a SPAM, return it for further processing Packet result = packet.swapElemFromTo(); addOutPacket(result); } public int processingThreads() { } 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.getElemFrom() != null) { return packet.getElemFrom().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; } public Map<String, Object> getDefaults(Map<String, Object> params) { Map<String, Object> defs = super.getDefaults(params); defs.put(BAD_WORDS_KEY, INITIAL_BAD_WORDS); defs.put(WHITELIST_KEY, INITIAL_WHITE_LIST); defs.put(PREPEND_TEXT_KEY, prependText); defs.put(SECURE_LOGGING_KEY, secureLogging); defs.put(ABUSE_ADDRESS_KEY, abuseAddress); defs.put(NOTIFICATION_FREQ_KEY, notificationFrequency); return defs; } public void setProperties(Map<String, Object> props) { super.setProperties(props); updateServiceDiscoveryItem(getName(), null, getDiscoDescription(), "automation", "spam-filtering", true, "tigase:x:spam-filter", "tigase:x:spam-reporting"); } public synchronized void everyMinute() { super.everyMinute(); if ((++delayCounter) >= notificationFrequency) { addOutPacket(Packet.getMessage(abuseAddress, getComponentId(), StanzaType.chat, "Detected spam messages: " + spamCounter, "Spam counter", null, newPacketId("spam-"))); delayCounter = 0; spamCounter = 0; } } return "Spam filtering"; } return "spam"; } public void getStatistics(StatisticsList list) { super.getStatistics(list); list.add(getName(), "Spam messages found", totalSpamCounter, list.add(getName(), "All messages processed", messagesCounter, // Some very expensive statistics generation code... } } public void initBindings(Bindings binds) { super.initBindings(binds); binds.put(BAD_WORDS_VAR, badWords); binds.put(WHITE_LIST_VAR, whiteList); } }
There are cases when you want to store some data permanently by your component. You can of course use the component configuration to provide some database connection settings, implement your own database connector and store records you need. There is, however, a very simple and useful framework which allows you to read and store some data transparently in either database or disk file. The framework also supports ad-hoc commands interface straight away so you can manipulate your component data using a good XMPP client.
This guide will teach you how to create a simple data repository and use it in your component. The repository can be automatically exposed via ad-hoc commands and you can change your data from any XMPP client.
The Tigase server offers an API to filter packets traffic inside every component. You can separately filter incoming and outgoing packets.
By filtering we understand intercepting a packet and possibly making some changes to the packet or just blocking the packet completely. By blocking we understand stopping from any further processing and just dropping the packet.
The packet filtering is based on the PacketFilterIfc interface. Please have a look in the JavaDoc documentation to this interface for all the details. The main filtering method is Packet filter(Packet packet); which takes packets as an input, processes it, possibly alerting the packet content (may add or remove some payloads) and returns a Packet for further processing. If it returns null it means the packet is blocked and no further processing is permitted otherwise it returns a Packet object which is either the same object it received as a parameter or a modified copy of the original object.
Please note, although Packet object is not unmodifiable instance it is recommended to not make any changes on the existing object. The same Packet might be processed at the same time by other components or threads, therefore modification of the Packet may lead to unpredictable results.
Please refer to an example code in PacketCounter which is a very simple filter counting different types of packets. This filter is by default loaded to all components which might be very helpful for assessing traffic shapes on newly deployed installation. You can get counters for all types of packets, where they are generated, where they flow, what component they put the most load on.
This is because packet filter can also generate and present own statistics which are accessible via normal statistics monitoring mechanisms. To take advantage of the statistics functionality the packet filter has to implement void getStatistics(StatisticsList list); method. Normally the method can be empty but you can generate and add to the list own statistics from the filter. Please refer to PacketCounter for an example implementation code.
Packet filters are configurable, that is a list of packet filters can be provided in the Tigase server configuration for each component separately and for each traffic direction. This gives you a great flexibility and control over the data flow inside the Tigase server.
You can, for example load specific packet filters to all connections managers to block specific traffic or specific packet source from sending messages to users on your server. You could also reduce the server overall load by removing certain payload from all packets. Possibilities are endless.
The default configuration is generated in such a way that each components loads a single packet filter - PacketCounter for each traffic direction:
message-router/incoming-filters=tigase.server.filters.PacketCounter message-router/outgoing-filters=tigase.server.filters.PacketCounter sess-man/incoming-filters=tigase.server.filters.PacketCounter sess-man/outgoing-filters=tigase.server.filters.PacketCounter c2s/incoming-filters=tigase.server.filters.PacketCounter c2s/outgoing-filters=tigase.server.filters.PacketCounter s2s/incoming-filters=tigase.server.filters.PacketCounter s2s/outgoing-filters=tigase.server.filters.PacketCounter bosh/incoming-filters=tigase.server.filters.PacketCounter bosh/outgoing-filters=tigase.server.filters.PacketCounter muc/incoming-filters=tigase.server.filters.PacketCounter muc/outgoing-filters=tigase.server.filters.PacketCounter
Now, let's say you have a packet filter implemented in class: com.company.SpamBlocker. You want to disable PacketCounter on most of the components leaving it only in the message router component and you want to install SpamBlocker in all connection managers.
Please note, in case of the connection managers 'incoming' and 'outgoing' traffic is probably somehow opposite from what you would normally expect.
According to above explanation we have to apply the SpamBlocker filter to all 'outgoing' traffic in all connection managers. At the second thought you may also decide that it might be actually useful to compare traffic shape between Bosh connections and standard XMPP c2s connections. So let's leave packet counters for this components too.
Here is our new configuration applying SpamBlocker to connection managers and PacketCounter to a few other components:
message-router/incoming-filters=tigase.server.filters.PacketCounter message-router/outgoing-filters=tigase.server.filters.PacketCounter sess-man/incoming-filters= sess-man/outgoing-filters= c2s/incoming-filters=tigase.server.filters.PacketCounter c2s/outgoing-filters=tigase.server.filters.PacketCounter,com.company.SpamBlocker s2s/incoming-filters= s2s/outgoing-filters=com.company.SpamBlocker bosh/incoming-filters=tigase.server.filters.PacketCounter bosh/outgoing-filters=tigase.server.filters.PacketCounter,com.company.SpamBlocker muc/incoming-filters= muc/outgoing-filters=
The simplest way, right now to apply the new configuration is via init.properties file which is in details described in the online documentation.
The component configuration API is actually very simple, it consists of two methods:
Map<String, Object> getDefaults(Map<String, Object> params); void setProperties(Map<String, Object> properties);
The first method retrieves configuration defaults from the component while the second sets the new configuration for the component. It does look very simple and it is very simple, however there is something more to know about that to use it effectively.
Before we go into all the details it might be very helpful to know the full component initialisation sequence, how the component is brought to life and when the configuration is set. Component loading and starting sequence looks like this:
setName(compName); method is called to set a name for the component. This method is (should) be called only once in the component live time.start(); method is called which starts all the component internal threads. This method, together with stop(); can be called many times to put the component processing on hold or restart processing. The developer should normally not be concerned about these, unless he decided to overwrite these methods.getDefaults(); method is called to retrieve initial settings for the component. This method is normally called only once in the component life time.setProperties(); is called to set new configuration for the component. This method can be called many times at any point during the component life time.initializationCompleted(); method is called to notify the component that the global server initialisation has been finished. This method is called only once during the server startup time, after all components have been initialised and configured. This method is mainly used by network connection managers which wait with activating socket listeners until the server is fully functional.The important thing about all the configuration stuff is that the component does not read/ask/request configuration. The configuration is pushed to the component by the configuration manager. The setProperties() method can be called at any time and any number of times while the server is running. This design allows for the server reconfiguration at run-time and developers should be aware of this and properly code the method to allow for the component reconfiguration at run-time.
Both API methods operate on Map<String, Object>, hence, essentially the component configuration is just a list of (key, value) pairs. The Object can any of following:
It is guaranteed that if the component returns a default configuration entry in any of above types, the setProperties() method sets the configuration entry in the same exact type. This is quite convenient as you can limit type conversions (numbers parsing for example) in your code.
Map<String, Object> getDefaults(Map<String, Object> params);
This method is normally called only once, just after the component instance has been created. It is used to get some initial settings from the component and create a default/initial configuration which can be modified by the user. It is recommended that the component returns all possible settings with it's default values so they can be presented to the end-user for configuration or diagnostic purposes. No component initialisation can take place here and the developer can not assume that this method is called only once. Every time this method is called it should return only defaults not the settings set with setProperties(). The Map<String, Object> params provided as a parameter to this method can contain some 'hints' or 'pre-initial' parameters which can affect generating default configuration. This is because configuration for some components may be complex and can have many different presets or optimisations depending on the use case. These presets can be used to generate proper default configuration. If the component implementation extends AbstractMessageReceiver then the implementation of the method should always look like this:
public Map<String, Object> getDefaults(Map<String, Object> params) { defs.put(CONF_ENTRY_KEY, conf_entry_val); return defs; }
void setProperties(Map<String, Object> properties);
This method is called to set configuration for the component. It can be called at any time and many times during the server run-time. The configuration will always contain all entries returned by getDefaults method but some of them might be overwritten by user provided settings. If the component implementation extends AbstractMessageReceiver then the implementation of the method should always look like this:
super.setProperties(properties); }
Normally configuration presets depend on the component implementation and are different for each component. There are a few presets however which are often used commonly by different components:
--test if set it means that the server runs in a test mode, which may mean different things for different components. The component may use this parameter to turn testing mode on.--admins if set it provides a list of administrator IDs. These user may have special access permissions for the component. They usually can execute administrator ad-hoc commands.--user-db-uri if set it contains the main database connection string. The component may keep there own data.There are some global settings which are provided to all components and can be used by all of them. Usually they point so some shared resources which can be used by all components.
SHARED_USER_REPO_PROP_KEY is a configuration key for the user repository instance. This instance can be shared among components and used to store component data in database as well as access to user data.
UserRepository user_repo; user_repo = (UserRepository) properties.get(SHARED_USER_REPO_PROP_KEY);
SHARED_USER_REPO_POOL_PROP_KEY is a configuration key for the user repository pool. In most cases the user repository is just an SQL database. To improve the access to the database a connection pool is created which is realised by creating many UserRepository instances connecting to the same database.
UserRepository user_repo; user_repo = (UserRepository) properties.get(SHARED_USER_REPO_POOL_PROP_KEY);
SHARED_AUTH_REPO_PROP_KEY is a configuration key for the authentication repository. Components normally do not need access to this repository unless they deal with user authentication and authentication data is kept separately from the rest of the user data.
UserAuthRepository auth_repo; auth_repo = (UserAuthRepository) properties.get(SHARED_AUTH_REPO_PROP_KEY);
List of documents describing how to work with sources and how to compile them.
The dependency for Tigase Utils Package has changed. This is important for everybody who builds the Tigase server manually from sources using Ant tool. The Maven handles all the dependencies automatically and scripts have been updated.
Please keep reading for more details how to compile the server from sources in current SVN repositories.
If you have an old Tigase MUC or Tigase Extras package lying in the server/libs/ directory please remove it now. You have to update it too and copy it over to the server/libs/ directory after you completed steps below.
For all those who build the server from sources manually using Ant here is a short guide:
This is a very short guide but I hope it helps. If you have any problems, please let me know.
Although the server doesn't need any third-party libraries apart from Java 6.0 (1.6beta2) compliant JVM to run, Apache Ant tool and Ant-Contrib are used to build binaries of Tigase applications and libraries. Another tools which is needed is a Subversion which is required to download the most recent sources from Tigase repository.
To make it a list, again:
Install all above in standard way, appropriate for your operating system. It is enough if they are available in system PATH variable so you can execute them from command line.
Tigase Server has been divided into a few smaller subprojects some time ago. In order to have it all working together we need to do compile them one by one. Here is step by step instruction how to do it. Assuming you already run command line shell and changed to directory where you want to keep all Tigase files do as follows:
svn co https://svn.tigase.org/reps/tigase-utils/trunk/ utils cd utils ant clean jar cd ..
svn co https://svn.tigase.org/reps/tigase-xmltools/trunk/ xmltools cd xmltools ant clean jar cd ..
svn co https://svn.tigase.org/reps/tigase-server/trunk/ server cp xmltools/jars/tigase-xmltools.jar server/libs/ cp utils/jars/tigase-utils.jar server/libs/ cd server ant clean jar
Now you have Tigase Server compiled and ready to run. To check and make sure it is indeed compiled and can be executed you can try to start the server. Assuming you are in the directory where you executed the last compilation command for server sources run following command:
java -cp libs/tigase-utils.jar:libs/tigase-xmltools.jar:jars/tigase-server.jar tigase.server.XMPPServer
If it all worked correctly you should see output similar to presented below:
2006-10-04 17:00:38 ConfigRepository.init() WARNING: Can not open existing configuration file 2006-10-04 17:00:38 XMLDB.setupNewDB() INFO: Create empty DB. 2006-10-04 17:00:38 MessageRouter.addRegistrator() INFO: Adding registrator: Configurator 2006-10-04 17:00:38 MessageRouter.addComponent() INFO: Adding component: Configurator 2006-10-04 17:00:38 Configurator.setupLogManager() WARNING: DONE 2006-10-04 17:00:38 Configurator.setupLogManager() WARNING: DONE 2006-10-04 17:00:39 XMLRepository.() WARNING: Can not open existing user repository file
Now you can proceed to configuration document to learn how to tweak server settings or you can just start hacking server code and do experiments.
Documents describing Maven use with the Tigase projects.
Thanks to bmalkow you can now build Tigase server from sources using Maven 2.x tool.
This should greatly simplify first steps with Tigase code and it was requested by many of those trying to get the server running from sources.
Maven repository with Tigase packages is located at address: maven.tigase.org.
Now all you need to compile sources and generate packages needed to run the server is just a few simple steps below:
svn co https://svn.tigase.org/reps/tigase-server/trunk/ tigase-server
cd tigase-server
mvn assembly:assembly
target. Go to this directory now:cd target/
and list content of this directory.
On Linux, Unix system:
ls -l
On MS Windows system:
dir
You should see at least 2 files like these:
tigase-server-2.4.0-SNAPSHOT-prodenv.tar.gz tigase-server-2.4.0-SNAPSHOT-prodenv.zip
tar -xzvf tigase-server-2.4.0-SNAPSHOT-prodenv.tar.gz
or
unzip tigase-server-2.4.0-SNAPSHOT-prodenv.zip
tigase-server-2.4.0-SNAPSHOT/. Now go to this directory:cd tigase-server-2.4.0-SNAPSHOT/
chmod u+x bin/*
./bin/tigase.sh run etc/tigase.conf
You can get a few warnings about missing configuration file (which will be automatically created) and user repository file (which will be automatically created when you register first user).
For your convenience there are a few other startup files in etc/ directory. You can look and modify them according to your needs.
If you don't use Maven at all or use it once a year you may find the document a useful maven commands reminder:
mvn compile - compilation of the snapshot packagemvn package - create snapshot jar filemvn install - install in local repository shanpshot jar filemvn deploy - deploy to the remote repository snapshot jar filemvn release:prepare prepare the project for a new version releasemvn release:perform execute new version release generationmvn -DdescriptorId=src assembly:assembly To generate installer:
build.xml which is in the src directory of IzPack install. Just enter this dir and type:ant all
src/lib directory or _build directory of IzPack. You may need to tweak the build.xml file which is in the same dir as the readme and point to the directory where IzPack compiled classess reside.
<!-- fragment -->
<classpath>
<pathelement location="java"/>
<!-- tweak below fragment -->
<pathelement location="${installer.path}/_build"/>
<pathelement location="${installer.path}/bin/panels/TargetPanel.jar"/>
</classpath>
generate-installer.sh script. Compiled custom panels will be placed here before running installer compiler.
script/generate-installer.sh. Change the IZPACK_DIR variable to point to the IzPack instalation directory e.g.IZPACK_DIR="/usr/local/IzPack421"
scripts/generate-installer.sh file you will find in the main server source code directory. You should start it from the server root dir.The guide contains description of non-standard or experimental functionality of the server. Some of them are based on never published extensions, some of them are just test implementation for new ideas or performance improvements.
Web clients have no way to store any data locally, on the client side. Therefore after a web page reload the web clients loses all the context it was running in before the page reload.
Some elements of the context can be retrieved from the server like the roster and all contacts presence information. Some other data however can not be restored easily like opened chat windows and the chat windows contents. Even if the roster restoring is possible, this operation is very expensive in terms of time and resources on the server side.
On of possible solutions is to allow web client to store some data in the Bosh component cache on the server side for the time while the Bosh session is active. After the page reload, if the client can somehow retrieve SID (stored in cookie or provided by the web application running the web client) it is possible to reload all the data stored in the Bosh cache to the client.
Bosh session context data are: roster, contacts presence information, opened chat windows, chat windows content and some other data not known at the moment. Ideally the web client should be able to store any data in the Bosh component cache it wants.
The Bosh Session Cache is divided into 2 parts - automatic cache and dynamic cache.
The reason for splitting the cache into 2 parts is that some data can be collected automatically by the Bosh component and it would be very inefficient to require the client to store the data in the Bosh cache. The best example for such data is the Roster and contacts presence information.
All the Bosh Session Cache actions are executed using additional <body/> element attributes: cache and cache-id. Attribute cache stores the action performed on the Bosh cache and the cache-id attribute refers to the cache element if the action attribute needs it. cache-id is optional. There is a default cache ID (empty one) associated with the elements for which the cache-id is not provided.
If the <body/> element contains the cache attribute it means that all data included in the <body/> refer to the cache action. It is not allowed, for example to send a message in the body and have the cache action set to get. The <body/> element with cache action get, get_all, on, off, remove must be empty. The <body/> element with actions set or add must contain data to store in the cache.
If the cache is set to off (the default value) all requests to the cache are ignored. This is to ensure backward compatibility with the original Bosh specification and to make sure that in default environment the Bosh component doesn't consume any extra resources for cache processing and storing as the cache wouldn't be used by the client anyway.
cache-id from the Bosh cache. Note there is no result cache action. The <body/> sent as a response from the server to the client may contain cache results for a given cache-id and it may also contain other data received by the Bosh component for the client. It may also happen that large cached data are split into a few parts and each part can be sent in a separate <body/> element. It may usually happen for the Roster data.<body/> element. The cache content will be divided into a smaller parts of a reasonable size and will be sent to the client in a separate <body/> elements. It may also happen that the <body/> element contain the cache elements as well as the new requests sent to the user like new messages or presence information.<body/> element. The only restriction is that the cached data must be a valid XML content. The data are returned to the client in exactly the same form as they were received from the server. The set action replaces any previously stored data under this ID.Cache ID can be any characters string. There might be some IDs reserved for a special cases, like for the Roster content. To avoid any future ID conflicts reserved ID values starts with: bosh - string.
There is a default cache ID - en empty string. Thus cache-id attribute can be omitted and then the requests refers to data stored under the default (empty) ID.
Here is a list of reserved Cache IDs:
The Bosh Cache might do or might not do optimizations on the roster like removing elements from the cached roster if the roster remove has been received or may just store all the roster requests and then send them all to the client.
There is a one mandatory optimization the Bosh Cache must perform. It must remember the last (and only the last) presence status for each roster item. Upon roster retrieving from the cache the Bosh component must send the roster item first and then the presence for the item. If the presence is missing it means off-line presence.
If the roster is small it can be sent to the client in a single packet but for a large roster it is recommended to split contact lists to batches of max 100 elements. The Bosh component may send all roster contacts first and then all presences or it can send a part of the roster, presences for sent items, next part of the roster, presences for next items and so on....
Normal roster contacts stored created as so called dynamic roster part are delivered to the end user transparently. The XMPP client doesn't really know what contacts come from his own static roster created manually by the user and what contacts come from dynamic roster part that is contacts and groups generated dynamically by the server logic.
Some specialized clients need to store extra bits of information about roster contacts. For the normal user static roster this extra information can be stored as private data and is available only to the this single user. In some cases however clients need to store information about contacts from the dynamic roster part and this information must be available to all users accessing dynamic roster part.
The protocol defined here allows exchanging information, saving and retrieving extra data about the contacts.
Extra contact data is accessed using IQ stanzas, specifically by means of a <query/> child element qualified by the 'jabber:iq:roster-dynamic' namespace. The <query/> child element MAY contain one or more <item/> children, each describing a unique contact item. Content of the <item/> element is not specified and is implementation dependent. From the Tigase server point of view it can contain any valid XML data. Whole <item/> element is passed to the DynamicRoster? implementation class as is and without any verification. Upon retrieving the contact extra data the DynamicRoster? implementation is supposed to provide a valid XML <item/> element with all the required data for requested 'jid'.
The 'jid' attribute specifies the Jabber Identifier (JID) that uniquely identifies the roster item. Inclusion of the 'jid' attribute is REQUIRED.
Following actions on the extra contact data are allowed:
Upon connecting to the server and becoming an active resource, a client can request the contact extra data. This request can be made either after or before requesting the user roster. The client's request for the extra contact data is OPTIONAL.
Example: Client requests contact extra data from the server using 'get' request:
<iq type='get' id='rce_1'>
<query xmlns='jabber:iq:roster-dynamic'>
<item jid='archimedes@eureka.com'/>
</query>
</iq>
Example: Client receives contact extra data from the server, but there were either no extra information for the user or the user was not found in the dynamic roster:
<iq type='result' id='rce_1'>
<query xmlns='jabber:iq:roster-dynamic'>
<item jid='archimedes@eureka.com'/>
</query>
</iq>
Example: Client receives contact extra data from the server, and there was some extra information found about the contact:
<iq type='result' id='rce_1'>
<query xmlns='jabber:iq:roster-dynamic'>
<item jid='archimedes@eureka.com'>
<phone>+12 3234 322342</phone>
<note>This is short note about the contact</note>
<fax>+98 2343 3453453</fax>
</item>
</query>
</iq>
At any time, a client MAY update contact extra information on the server.
Example: Client sends contact extra information using 'set' request.
<iq type='set' id='a78b4q6ha463'>
<query xmlns='jabber:iq:roster-dynamic'>
<item jid='archimedes@eureka.com'>
<phone>+22 3344 556677</phone>
<note>he is a smart guy, he knows whether the crown is made from pure gold or not.</note>
</item>
</query>
</iq>
Client responds to the server:
<iq type='result' id='a78b4q6ha463'/>
A client MAY update contact extra information for more than a single item in one request:
Example: Client sends contact extra information using 'set' request with many <item/> elements.
<iq type='set' id='a78b4q6ha464'>
<query xmlns='jabber:iq:roster-dynamic'>
<item jid='archimedes@eureka.com'>
<phone>+22 3344 556677</phone>
<note>he is a smart guy, he knows whether the crown is made from pure gold or not.</note>
</item>
<item jid='newton@eureka.com'>
<phone>+22 3344 556688</phone>
<note>He knows how heavy I am.</note>
</item>
<item jid='pascal@eureka.com'>
<phone>+22 3344 556699</phone>
<note>This guy helped me cure my sickness!</note>
</item>
</query>
</iq>
Client responds to the server:
<iq type='result' id='a78b4q6ha464'/>